openbrawl 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/ui/BuddyBattle.tsx
3
+ import { useState, useCallback } from "react";
4
+ import { Box, Text, useInput, useStdout } from "ink";
5
+ import { parseBuddyCard } from "../buddy/parse.js";
6
+ import { buildBuddyBattlePrompt } from "../buddy/prompts.js";
7
+ import { parseBattleRound } from "../buddy/battle.js";
8
+ import { callClaudeAsync } from "../llm/claude.js";
9
+ import { exec } from "child_process";
10
+ function BuddyStatLine({ buddy, color }) {
11
+ return (_jsx(Text, { color: color, children: buddy.stats.map((s) => `${s.name.slice(0, 3).toUpperCase()}:${s.value}`).join(" ") }));
12
+ }
13
+ function BuddyCardDisplay({ buddy, color, compact }) {
14
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: color, paddingX: 1, children: [buddy.rarity && _jsxs(Text, { color: color, dimColor: true, children: [buddy.rarity, buddy.species ? ` · ${buddy.species}` : ""] }), !compact && buddy.asciiArt && (_jsx(Text, { color: color === "cyan" ? "cyanBright" : "yellowBright", children: buddy.asciiArt })), _jsx(Text, { bold: true, color: color, children: buddy.name }), !compact && buddy.description && _jsxs(Text, { dimColor: true, italic: true, children: ["\"", buddy.description, "\""] }), _jsx(Box, { flexDirection: "column", children: buddy.stats.map((s, i) => (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: s.name.padEnd(12) }), _jsxs(Text, { color: color, children: ["█".repeat(Math.round(s.value / 10)), "░".repeat(10 - Math.round(s.value / 10))] }), _jsxs(Text, { children: [" ", s.value] })] }, i))) })] }));
15
+ }
16
+ export function BuddyBattle({ onQuit }) {
17
+ const { stdout } = useStdout();
18
+ const termHeight = stdout.rows ?? 40;
19
+ const [phase, setPhase] = useState("loading_player");
20
+ const [playerBuddy, setPlayerBuddy] = useState(null);
21
+ const [opponentBuddy, setOpponentBuddy] = useState(null);
22
+ const [rounds, setRounds] = useState([]);
23
+ const [currentRound, setCurrentRound] = useState(1);
24
+ const [currentBeat, setCurrentBeat] = useState(0);
25
+ const [currentBeats, setCurrentBeats] = useState([]);
26
+ const [playerScore, setPlayerScore] = useState(0);
27
+ const [opponentScore, setOpponentScore] = useState(0);
28
+ const [error, setError] = useState("");
29
+ const [loadingMsg, setLoadingMsg] = useState("");
30
+ const [victoryQuip, setVictoryQuip] = useState("");
31
+ const [matchWinner, setMatchWinner] = useState("");
32
+ const clearScreen = useCallback(() => {
33
+ stdout.write("\x1B[2J\x1B[H");
34
+ }, [stdout]);
35
+ function loadBuddyFromClipboard() {
36
+ setLoadingMsg("Reading clipboard...");
37
+ setError("");
38
+ exec(`pbpaste`, { encoding: "utf-8", timeout: 5000, maxBuffer: 1024 * 1024 }, (err, out) => {
39
+ if (err) {
40
+ setError("Could not read clipboard. Copy your buddy card and try again.");
41
+ return;
42
+ }
43
+ try {
44
+ const buddy = parseBuddyCard(out);
45
+ setError("");
46
+ if (phase === "loading_player") {
47
+ setPlayerBuddy(buddy);
48
+ clearScreen();
49
+ setPhase("loading_opponent");
50
+ }
51
+ else if (phase === "loading_opponent") {
52
+ setOpponentBuddy(buddy);
53
+ clearScreen();
54
+ setPhase("vs");
55
+ }
56
+ }
57
+ catch (parseErr) {
58
+ setError(`Parse error: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}. Copy the full buddy card and try again.`);
59
+ }
60
+ });
61
+ }
62
+ async function startRound(roundNum, prevRounds, pScore, oScore) {
63
+ if (!playerBuddy || !opponentBuddy)
64
+ return;
65
+ setLoadingMsg(`Claude is narrating Round ${roundNum}...`);
66
+ clearScreen();
67
+ setPhase("waiting");
68
+ try {
69
+ const isMatchPoint = pScore === 1 || oScore === 1;
70
+ const prompt = buildBuddyBattlePrompt(playerBuddy, opponentBuddy, roundNum, prevRounds, isMatchPoint);
71
+ const raw = await callClaudeAsync(prompt.system, prompt.user);
72
+ const round = parseBattleRound(raw, roundNum, [playerBuddy.name, opponentBuddy.name]);
73
+ setCurrentBeats(round.beats);
74
+ setCurrentBeat(0);
75
+ const newRounds = [...prevRounds, round];
76
+ setRounds(newRounds);
77
+ const newPScore = round.winner === playerBuddy.name ? pScore + 1 : pScore;
78
+ const newOScore = round.winner === opponentBuddy.name ? oScore + 1 : oScore;
79
+ setPlayerScore(newPScore);
80
+ setOpponentScore(newOScore);
81
+ if (round.victoryQuip) {
82
+ setVictoryQuip(round.victoryQuip);
83
+ }
84
+ if (newPScore === 2 || newOScore === 2) {
85
+ setMatchWinner(round.winner);
86
+ }
87
+ clearScreen();
88
+ setPhase("battle");
89
+ }
90
+ catch (err) {
91
+ setError(`Battle error: ${err instanceof Error ? err.message : String(err)}`);
92
+ clearScreen();
93
+ setPhase("vs");
94
+ }
95
+ }
96
+ useInput((input, key) => {
97
+ if (input === "q" || input === "Q" || (key.escape && phase !== "loading_player" && phase !== "loading_opponent")) {
98
+ onQuit();
99
+ return;
100
+ }
101
+ if ((phase === "loading_player" || phase === "loading_opponent") && (input === "v" || input === "V")) {
102
+ loadBuddyFromClipboard();
103
+ return;
104
+ }
105
+ if (phase === "vs" && (input === " " || key.return)) {
106
+ startRound(1, [], 0, 0);
107
+ return;
108
+ }
109
+ if (phase === "battle" && (input === " " || key.return)) {
110
+ if (currentBeat < currentBeats.length - 1) {
111
+ setCurrentBeat((b) => b + 1);
112
+ }
113
+ else {
114
+ // All beats revealed
115
+ if (matchWinner) {
116
+ clearScreen();
117
+ setPhase("victory");
118
+ }
119
+ else {
120
+ clearScreen();
121
+ setPhase("between_rounds");
122
+ }
123
+ }
124
+ return;
125
+ }
126
+ if (phase === "between_rounds" && (input === " " || key.return)) {
127
+ const nextRound = currentRound + 1;
128
+ setCurrentRound(nextRound);
129
+ startRound(nextRound, rounds, playerScore, opponentScore);
130
+ return;
131
+ }
132
+ if (phase === "victory") {
133
+ if (input === "r" || input === "R") {
134
+ // Rematch — same buddies
135
+ setRounds([]);
136
+ setCurrentRound(1);
137
+ setCurrentBeat(0);
138
+ setCurrentBeats([]);
139
+ setPlayerScore(0);
140
+ setOpponentScore(0);
141
+ setMatchWinner("");
142
+ setVictoryQuip("");
143
+ clearScreen();
144
+ setPhase("vs");
145
+ return;
146
+ }
147
+ if (input === "n" || input === "N") {
148
+ // New buddies
149
+ setPlayerBuddy(null);
150
+ setOpponentBuddy(null);
151
+ setRounds([]);
152
+ setCurrentRound(1);
153
+ setCurrentBeat(0);
154
+ setCurrentBeats([]);
155
+ setPlayerScore(0);
156
+ setOpponentScore(0);
157
+ setMatchWinner("");
158
+ setVictoryQuip("");
159
+ setError("");
160
+ clearScreen();
161
+ setPhase("loading_player");
162
+ return;
163
+ }
164
+ }
165
+ });
166
+ // ─── LOADING PLAYER BUDDY ────────────────
167
+ if (phase === "loading_player") {
168
+ return (_jsx(Box, { flexDirection: "column", height: termHeight, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: "red", children: "Buddy" }), " ", _jsx(Text, { color: "cyan", children: "Battle" })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Copy your buddy card to clipboard, then press ", _jsx(Text, { bold: true, color: "cyan", children: "V" }), " to load it."] }), _jsx(Text, { children: " " }), error && _jsx(Text, { color: "red", children: error }), _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { bold: true, children: "Q" }), " to go back"] })] }) }));
169
+ }
170
+ // ─── LOADING OPPONENT BUDDY ──────────────
171
+ if (phase === "loading_opponent") {
172
+ return (_jsx(Box, { flexDirection: "column", height: termHeight, children: _jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 1, children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: "red", children: "Buddy" }), " ", _jsx(Text, { color: "cyan", children: "Battle" })] }), _jsx(Text, { children: " " }), playerBuddy && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "cyan", children: ["Your buddy: ", _jsx(Text, { bold: true, children: playerBuddy.name }), " loaded!"] }) })), _jsxs(Text, { children: ["Copy your ", _jsx(Text, { bold: true, color: "red", children: "opponent's" }), " buddy card to clipboard, then press ", _jsx(Text, { bold: true, color: "cyan", children: "V" }), "."] }), _jsx(Text, { children: " " }), error && _jsx(Text, { color: "red", children: error }), _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { bold: true, children: "Q" }), " to go back"] })] }) }));
173
+ }
174
+ // ─── WAITING FOR CLAUDE ──────────────────
175
+ if (phase === "waiting") {
176
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, alignItems: "center", justifyContent: "center", children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: "red", children: "Buddy" }), " ", _jsx(Text, { color: "cyan", children: "Battle" })] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["[...] ", loadingMsg] }), _jsx(Text, { dimColor: true, children: "(Calling Claude \u2014 this may take a few seconds)" })] }));
177
+ }
178
+ // ─── VS SCREEN ───────────────────────────
179
+ if (phase === "vs" && playerBuddy && opponentBuddy) {
180
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "-- BUDDY BATTLE --" }), _jsx(Text, { children: " " })] }), _jsxs(Box, { justifyContent: "center", gap: 2, children: [_jsx(Box, { width: "40%", children: _jsx(BuddyCardDisplay, { buddy: playerBuddy, color: "cyan" }) }), _jsx(Box, { alignItems: "center", children: _jsx(Text, { bold: true, color: "red", children: "VS" }) }), _jsx(Box, { width: "40%", children: _jsx(BuddyCardDisplay, { buddy: opponentBuddy, color: "red" }) })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { bold: true, color: "white", children: "SPACE" }), " to begin battle"] }) })] }));
181
+ }
182
+ // ─── BATTLE (beat-by-beat) ───────────────
183
+ if (phase === "battle" && playerBuddy && opponentBuddy) {
184
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", alignItems: "center", children: [playerBuddy.asciiArt && _jsx(Text, { color: "cyanBright", children: playerBuddy.asciiArt }), _jsx(Text, { bold: true, color: "cyan", children: playerBuddy.name }), _jsx(BuddyStatLine, { buddy: playerBuddy, color: "cyan" })] }), _jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", children: [_jsxs(Text, { bold: true, color: "red", children: ["ROUND ", currentRound, " of 3"] }), _jsxs(Text, { dimColor: true, children: [playerScore, " \u2014 ", opponentScore] })] }), _jsxs(Box, { flexDirection: "column", alignItems: "center", children: [opponentBuddy.asciiArt && _jsx(Text, { color: "yellowBright", children: opponentBuddy.asciiArt }), _jsx(Text, { bold: true, color: "red", children: opponentBuddy.name }), _jsx(BuddyStatLine, { buddy: opponentBuddy, color: "red" })] })] }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(stdout.columns ?? 80, 80)) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, paddingY: 1, children: currentBeats.slice(0, currentBeat + 1).map((beat, i) => (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: beat }) }, i))) }), _jsx(Box, { children: currentBeat < currentBeats.length - 1 ? (_jsx(Text, { dimColor: true, italic: true, children: " Press SPACE for next beat..." })) : matchWinner ? (_jsx(Text, { dimColor: true, italic: true, children: " Press SPACE for victory screen..." })) : (_jsxs(Text, { dimColor: true, italic: true, children: [" Press SPACE for Round ", currentRound + 1, "..."] })) })] }));
185
+ }
186
+ // ─── BETWEEN ROUNDS ─────────────────────
187
+ if (phase === "between_rounds" && playerBuddy && opponentBuddy) {
188
+ const lastRound = rounds[rounds.length - 1];
189
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, alignItems: "center", justifyContent: "center", children: [_jsxs(Text, { bold: true, children: [_jsx(Text, { color: "red", children: "Buddy" }), " ", _jsx(Text, { color: "cyan", children: "Battle" })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Round ", currentRound, " goes to ", _jsx(Text, { bold: true, color: lastRound?.winner === playerBuddy.name ? "cyan" : "red", children: lastRound?.winner }), "!"] }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [playerBuddy.name, " ", _jsx(Text, { color: "cyan", children: playerScore }), " \u2014 ", _jsx(Text, { color: "red", children: opponentScore }), " ", opponentBuddy.name] }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: ["Press ", _jsx(Text, { bold: true, color: "white", children: "SPACE" }), " for Round ", currentRound + 1] })] }));
190
+ }
191
+ // ─── VICTORY ─────────────────────────────
192
+ if (phase === "victory" && playerBuddy && opponentBuddy) {
193
+ const winner = matchWinner === playerBuddy.name ? playerBuddy : opponentBuddy;
194
+ const winnerColor = matchWinner === playerBuddy.name ? "cyan" : "red";
195
+ return (_jsxs(Box, { flexDirection: "column", height: termHeight, alignItems: "center", paddingY: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "-- VICTORY --" }), _jsx(Text, { children: " " }), winner.asciiArt && _jsx(Text, { color: winnerColor, children: winner.asciiArt }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, color: winnerColor, children: [winner.name, " wins ", playerScore, "-", opponentScore, "!"] }), victoryQuip && (_jsxs(Text, { dimColor: true, italic: true, children: ["\"", victoryQuip, "\""] })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { bold: true, color: "white", children: "R" }), _jsx(Text, { dimColor: true, children: " Rematch " }), _jsx(Text, { bold: true, color: "white", children: "N" }), _jsx(Text, { dimColor: true, children: " New Buddies " }), _jsx(Text, { bold: true, color: "white", children: "Q" }), _jsx(Text, { dimColor: true, children: " Quit" })] })] }));
196
+ }
197
+ // Fallback
198
+ return _jsx(Text, { children: "Loading..." });
199
+ }
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { Box, Text } from "ink";
4
+ function HPBar({ hp }) {
5
+ const pct = hp.maxHP > 0 ? hp.currentHP / hp.maxHP : 0;
6
+ const barWidth = 20;
7
+ const filled = Math.round(pct * barWidth);
8
+ const color = hp.currentHP <= 0 ? "red" : pct > 0.5 ? "green" : pct > 0.25 ? "yellow" : "red";
9
+ const bar = "#".repeat(filled) + ".".repeat(barWidth - filled);
10
+ return (_jsxs(Text, { children: [_jsx(Text, { color: hp.team === "player" ? "cyan" : "red", children: hp.name.padEnd(14) }), _jsx(Text, { color: color, children: bar }), _jsxs(Text, { dimColor: true, children: [" ", hp.currentHP <= 0 ? " DEAD" : `${String(hp.currentHP).padStart(3)}/${hp.maxHP}`] })] }));
11
+ }
12
+ function EventLine({ event }) {
13
+ const isAbility = !!event.abilityTriggered;
14
+ const isBlocked = event.damageFormula?.includes("chip");
15
+ if (event.attacker === "Arena") {
16
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: " ARENA" }), _jsx(Text, { dimColor: true, children: " deals " }), _jsx(Text, { color: "red", children: event.damage }), _jsx(Text, { dimColor: true, children: " to " }), _jsx(Text, { children: event.defender }), event.knockedOut && _jsx(Text, { color: "red", bold: true, children: " KO!" })] }));
17
+ }
18
+ if (isAbility && event.damage <= 0) {
19
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", children: [" * ", event.attacker] }), _jsx(Text, { dimColor: true, children: " triggers " }), _jsx(Text, { color: "yellow", bold: true, children: event.abilityTriggered }), event.defender !== event.attacker && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: " on " }), _jsx(Text, { children: event.defender })] })), event.abilityEffect && _jsxs(Text, { dimColor: true, children: [" -- ", event.abilityEffect] })] }));
20
+ }
21
+ if (isAbility && event.damage > 0) {
22
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", children: [" * ", event.attacker] }), _jsx(Text, { dimColor: true, children: " triggers " }), _jsx(Text, { color: "yellow", bold: true, children: event.abilityTriggered }), _jsx(Text, { dimColor: true, children: " on " }), _jsx(Text, { children: event.defender }), _jsxs(Text, { color: "red", bold: true, children: [" -", event.damage] }), event.knockedOut && _jsx(Text, { color: "red", bold: true, children: " KO!" })] }));
23
+ }
24
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [" x ", event.attacker] }), _jsx(Text, { dimColor: true, children: " attacks " }), _jsx(Text, { children: event.defender }), isBlocked ? (_jsx(Text, { color: "blue", children: " BLOCKED (1 chip)" })) : (_jsxs(Text, { color: "red", bold: true, children: [" -", event.damage] })), event.knockedOut && _jsx(Text, { color: "red", bold: true, children: " KO!" })] }));
25
+ }
26
+ function groupByTurn(events) {
27
+ const groups = [];
28
+ for (const e of events) {
29
+ const existing = groups.find((g) => g.turn === e.turn);
30
+ if (existing) {
31
+ existing.events.push(e);
32
+ }
33
+ else {
34
+ groups.push({ turn: e.turn, events: [e] });
35
+ }
36
+ }
37
+ return groups;
38
+ }
39
+ // Count how many terminal lines a turn group takes (1 for header + 1 per event)
40
+ function turnLineCount(group) {
41
+ return 1 + group.events.length;
42
+ }
43
+ // Get the tail of turns that fit within maxLines
44
+ function tailTurns(turns, maxLines) {
45
+ let lines = 0;
46
+ const result = [];
47
+ for (let i = turns.length - 1; i >= 0; i--) {
48
+ const count = turnLineCount(turns[i]);
49
+ if (lines + count > maxLines)
50
+ break;
51
+ lines += count;
52
+ result.unshift(turns[i]);
53
+ }
54
+ return result;
55
+ }
56
+ // Animated version — reveals one turn at a time, scrolling window
57
+ export function AnimatedCombatLog({ result, onComplete, maxLogLines, }) {
58
+ const turnGroups = groupByTurn(result.events);
59
+ const [visibleTurns, setVisibleTurns] = useState(0);
60
+ const [done, setDone] = useState(false);
61
+ useEffect(() => {
62
+ if (visibleTurns >= turnGroups.length) {
63
+ const t = setTimeout(() => setDone(true), 400);
64
+ return () => clearTimeout(t);
65
+ }
66
+ const delay = turnGroups.length > 15 ? 100 : turnGroups.length > 8 ? 200 : 350;
67
+ const t = setTimeout(() => setVisibleTurns((v) => v + 1), delay);
68
+ return () => clearTimeout(t);
69
+ }, [visibleTurns, turnGroups.length]);
70
+ useEffect(() => {
71
+ if (done)
72
+ onComplete();
73
+ }, [done, onComplete]);
74
+ const allShown = turnGroups.slice(0, visibleTurns);
75
+ // Only display the tail that fits
76
+ const displayTurns = tailTurns(allShown, maxLogLines - 3); // -3 for header + footer + spacer
77
+ const lastEvent = allShown.length > 0
78
+ ? allShown[allShown.length - 1].events[allShown[allShown.length - 1].events.length - 1]
79
+ : null;
80
+ const skippedTurns = allShown.length - displayTurns.length;
81
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "COMBATANTS" }), lastEvent?.hpAfter ? (lastEvent.hpAfter.filter((hp) => hp.maxHP > 0).map((hp) => (_jsx(HPBar, { hp: hp }, hp.name)))) : (_jsx(Text, { dimColor: true, children: "Preparing..." }))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, dimColor: true, children: ["COMBAT LOG -- turn ", Math.min(visibleTurns, turnGroups.length), "/", turnGroups.length, !done && _jsx(Text, { color: "yellow", children: " ..." })] }), skippedTurns > 0 && (_jsxs(Text, { dimColor: true, children: [" (", skippedTurns, " earlier turns)"] })), displayTurns.map((group) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, bold: true, children: [" TURN ", group.turn] }), group.events.map((e, i) => (_jsx(EventLine, { event: e }, i)))] }, group.turn))), done && (_jsx(_Fragment, { children: result.draw ? (_jsx(Text, { color: "yellow", bold: true, children: "=== DRAW ===" })) : result.playerWon ? (_jsxs(Text, { color: "cyan", bold: true, children: ["=== VICTORY === Survivors: ", result.playerTeamSurvivors.join(", ")] })) : (_jsxs(Text, { color: "red", bold: true, children: ["=== DEFEAT === Rival survivors: ", result.rivalTeamSurvivors.join(", ")] })) }))] })] }));
82
+ }
83
+ // Static version — for reviewing after battle, also windowed
84
+ export function CombatLog({ result, maxLogLines }) {
85
+ const turnGroups = groupByTurn(result.events);
86
+ const lastEvent = result.events[result.events.length - 1];
87
+ const displayTurns = tailTurns(turnGroups, maxLogLines - 3);
88
+ const skippedTurns = turnGroups.length - displayTurns.length;
89
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: "FINAL STATE" }), lastEvent?.hpAfter?.filter((hp) => hp.maxHP > 0).map((hp) => (_jsx(HPBar, { hp: hp }, hp.name)))] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, dimColor: true, children: ["COMBAT LOG -- ", turnGroups.length, " turns, ", result.events.length, " actions"] }), skippedTurns > 0 && (_jsxs(Text, { dimColor: true, children: [" (", skippedTurns, " earlier turns)"] })), displayTurns.map((group) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, bold: true, children: [" TURN ", group.turn] }), group.events.map((e, i) => (_jsx(EventLine, { event: e }, i)))] }, group.turn))), result.draw ? (_jsx(Text, { color: "yellow", bold: true, children: "=== DRAW ===" })) : result.playerWon ? (_jsxs(Text, { color: "cyan", bold: true, children: ["=== VICTORY === Survivors: ", result.playerTeamSurvivors.join(", ")] })) : (_jsxs(Text, { color: "red", bold: true, children: ["=== DEFEAT === Rival survivors: ", result.rivalTeamSurvivors.join(", ")] }))] })] }));
90
+ }
@@ -0,0 +1,10 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ export function DetailOverlay({ overlay, playerTeam, rivalTeam, }) {
5
+ const allCreatures = [
6
+ ...playerTeam.map((c) => ({ ...c, side: "player" })),
7
+ ...rivalTeam.map((c) => ({ ...c, side: "rival" })),
8
+ ];
9
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, width: "100%", children: [_jsxs(Text, { bold: true, color: "yellow", children: [overlay === "stats" && "-- DETAILED STATS --", overlay === "abilities" && "-- ABILITIES --", overlay === "mutations" && "-- MUTATIONS --"] }), _jsx(Text, { dimColor: true, children: "ESC to close" }), _jsx(Text, { children: " " }), overlay === "stats" && allCreatures.map((c, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: c.side === "player" ? "cyan" : "red", children: [c.name, " ", c.side === "rival" ? "(rival)" : "(you)"] }), _jsxs(Text, { children: [_jsxs(Text, { color: "red", bold: true, children: ["ATK ", String(c.stats.ATK).padStart(3)] }), " ", _jsxs(Text, { dimColor: true, children: ["#".repeat(Math.min(c.stats.ATK, 30)), ".".repeat(Math.max(0, 30 - c.stats.ATK))] })] }), _jsxs(Text, { children: [_jsxs(Text, { color: "blue", bold: true, children: ["DEF ", String(c.stats.DEF).padStart(3)] }), " ", _jsxs(Text, { dimColor: true, children: ["#".repeat(Math.min(c.stats.DEF, 30)), ".".repeat(Math.max(0, 30 - c.stats.DEF))] })] }), _jsxs(Text, { children: [_jsxs(Text, { color: "green", bold: true, children: ["SPD ", String(c.stats.SPD).padStart(3)] }), " ", _jsxs(Text, { dimColor: true, children: ["#".repeat(Math.min(c.stats.SPD, 30)), ".".repeat(Math.max(0, 30 - c.stats.SPD))] })] }), _jsxs(Text, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["HP ", String(c.stats.HP).padStart(3)] }), " ", _jsxs(Text, { dimColor: true, children: ["#".repeat(Math.min(c.stats.HP, 30)), ".".repeat(Math.max(0, 30 - c.stats.HP))] })] })] }, i))), overlay === "abilities" && allCreatures.map((c, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: c.side === "player" ? "cyan" : "red", children: [c.name, " ", c.side === "rival" ? "(rival)" : "(you)"] }), c.abilities.length === 0 ? (_jsx(Text, { dimColor: true, children: " No abilities yet" })) : (c.abilities.map((a, j) => (_jsxs(React.Fragment, { children: [_jsxs(Text, { children: [" ", _jsx(Text, { color: "yellow", bold: true, children: a.name }), _jsxs(Text, { dimColor: true, children: [" - ", a.description] })] }), _jsxs(Text, { dimColor: true, children: [" ", a.trigger.replace(/_/g, " "), " | ", a.effect, " | ", a.value, "% | ", a.target] })] }, j))))] }, i))), overlay === "mutations" && allCreatures.map((c, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: c.side === "player" ? "cyan" : "red", children: [c.name, " ", c.side === "rival" ? "(rival)" : "(you)"] }), c.mutations.length === 0 ? (_jsx(Text, { dimColor: true, children: " No mutations yet" })) : (c.mutations.map((m, j) => (_jsxs(Text, { color: "magenta", children: [" + ", m] }, j))))] }, i)))] }));
10
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function StatCard({ creature, isRival }) {
4
+ const color = isRival ? "red" : "cyan";
5
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: color, paddingX: 1, children: [_jsx(Text, { bold: true, color: color, children: creature.name }), _jsx(Text, { dimColor: true, italic: true, children: creature.flavorText }), _jsx(Text, { children: " " }), _jsxs(Box, { gap: 3, children: [_jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "ATK" }), " ", creature.stats.ATK] }), _jsxs(Text, { children: [_jsx(Text, { color: "blue", bold: true, children: "DEF" }), " ", creature.stats.DEF] }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "SPD" }), " ", creature.stats.SPD] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "HP" }), " ", creature.stats.HP] })] }), creature.abilities.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "ABILITIES" }), creature.abilities.map((a, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: a.name }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", a.description] })] }, i)))] })), creature.mutations.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "MUTATIONS" }), creature.mutations.slice(0, 3).map((m, i) => (_jsxs(Text, { color: "magenta", children: ["+ ", m] }, i))), creature.mutations.length > 3 && (_jsxs(Text, { dimColor: true, children: [" ...and ", creature.mutations.length - 3, " more"] }))] }))] }));
6
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "openbrawl",
3
+ "version": "0.2.2",
4
+ "description": "AI-powered auto-battler in your terminal. Type a prompt. Forge a creature. Watch it fight.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "keywords": ["game", "auto-battler", "ai", "cli", "terminal", "tui", "claude", "ink"],
7
+ "homepage": "https://openbrawl.fun",
8
+ "type": "module",
9
+ "bin": {
10
+ "openbrawl": "./dist/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/cli.js",
15
+ "dev": "tsx src/cli.tsx"
16
+ },
17
+ "dependencies": {
18
+ "ink": "^5.2.0",
19
+ "ink-text-input": "^6.0.0",
20
+ "ink-select-input": "^6.0.0",
21
+ "ink-spinner": "^5.0.0",
22
+ "react": "^18.3.1"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^18.3.0",
26
+ "tsx": "^4.19.0",
27
+ "typescript": "^5.6.0"
28
+ }
29
+ }