feat: integrate initiative tracker into campaign view
Wires up InitiativeTracker and CombatStartModal into CampaignView with socket event handlers, a DM-only Combat button, two-column layout with combat sidebar, and responsive CSS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
65c914e3e0
commit
247379ba57
2 changed files with 113 additions and 17 deletions
|
|
@ -165,3 +165,54 @@
|
|||
font-style: italic;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* ── Combat layout ── */
|
||||
|
||||
.content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.content.withCombat {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.combatSidebar {
|
||||
width: 190px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid rgba(var(--gold-rgb), 0.15);
|
||||
padding-right: 0.5rem;
|
||||
margin-right: 0.75rem;
|
||||
min-height: 300px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.content.withCombat .grid {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.addBtnActive {
|
||||
background: rgba(var(--gold-rgb), 0.15) !important;
|
||||
border: 1px solid rgba(var(--gold-rgb), 0.5) !important;
|
||||
color: var(--gold) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content.withCombat {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.combatSidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(var(--gold-rgb), 0.15);
|
||||
padding-right: 0;
|
||||
margin-right: 0;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ import {
|
|||
undoRoll,
|
||||
} from "../api";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import type { Character, Gear, Talent, GameItem, RollResult } from "../types";
|
||||
import type { Character, Gear, Talent, GameItem, RollResult, CombatState } from "../types.js";
|
||||
import InitiativeTracker from "../components/InitiativeTracker.js";
|
||||
import CombatStartModal from "../components/CombatStartModal.js";
|
||||
import CharacterCard from "../components/CharacterCard";
|
||||
import CharacterDetail from "../components/CharacterDetail";
|
||||
import RollLog from "../components/RollLog";
|
||||
|
|
@ -54,6 +56,8 @@ export default function CampaignView() {
|
|||
const pendingRollRef = useRef<RollResult | null>(null);
|
||||
const [atmosphere, setAtmosphere] = useState<AtmosphereState>(defaultAtmosphere);
|
||||
const [focusSpells, setFocusSpells] = useState<Map<number, string>>(new Map());
|
||||
const [combats, setCombats] = useState<CombatState[]>([]);
|
||||
const [showCombatStart, setShowCombatStart] = useState(false);
|
||||
|
||||
function handleAtmosphereChange(next: AtmosphereState) {
|
||||
setAtmosphere(next);
|
||||
|
|
@ -70,6 +74,7 @@ export default function CampaignView() {
|
|||
if (c) setCampaignName(c.name);
|
||||
});
|
||||
socket.emit("join-campaign", String(campaignId));
|
||||
socket.emit("initiative:request-state", campaignId);
|
||||
return () => {
|
||||
socket.emit("leave-campaign", String(campaignId));
|
||||
};
|
||||
|
|
@ -256,6 +261,14 @@ export default function CampaignView() {
|
|||
void characterId;
|
||||
}
|
||||
|
||||
function onInitiativeState(data: CombatState[]) {
|
||||
setCombats(data);
|
||||
}
|
||||
|
||||
function onInitiativeUpdated(data: CombatState[]) {
|
||||
setCombats(data);
|
||||
}
|
||||
|
||||
socket.on("character:created", onCharacterCreated);
|
||||
socket.on("character:updated", onCharacterUpdated);
|
||||
socket.on("character:deleted", onCharacterDeleted);
|
||||
|
|
@ -268,6 +281,8 @@ export default function CampaignView() {
|
|||
socket.on("atmosphere:update", onAtmosphereUpdate);
|
||||
socket.on("roll:undone", onRollUndone);
|
||||
socket.on("character:rested", onCharacterRested);
|
||||
socket.on("initiative:state", onInitiativeState);
|
||||
socket.on("initiative:updated", onInitiativeUpdated);
|
||||
|
||||
return () => {
|
||||
socket.off("character:created", onCharacterCreated);
|
||||
|
|
@ -282,6 +297,8 @@ export default function CampaignView() {
|
|||
socket.off("atmosphere:update", onAtmosphereUpdate);
|
||||
socket.off("roll:undone", onRollUndone);
|
||||
socket.off("character:rested", onCharacterRested);
|
||||
socket.off("initiative:state", onInitiativeState);
|
||||
socket.off("initiative:updated", onInitiativeUpdated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -387,6 +404,14 @@ export default function CampaignView() {
|
|||
onAtmosphereChange={handleAtmosphereChange}
|
||||
/>
|
||||
)}
|
||||
{role === "dm" && (
|
||||
<button
|
||||
className={`${styles.addBtn} ${combats.length > 0 ? styles.addBtnActive : ""}`}
|
||||
onClick={() => setShowCombatStart(true)}
|
||||
>
|
||||
⚔ Combat
|
||||
</button>
|
||||
)}
|
||||
{role === "dm" && (
|
||||
<button className={styles.addBtn} onClick={handleInvite}>
|
||||
Invite Player
|
||||
|
|
@ -401,6 +426,18 @@ export default function CampaignView() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.content} ${combats.length > 0 ? styles.withCombat : ""}`}>
|
||||
{combats.length > 0 && (
|
||||
<div className={styles.combatSidebar}>
|
||||
<InitiativeTracker
|
||||
combat={combats[0]}
|
||||
characters={characters}
|
||||
isDM={role === "dm"}
|
||||
currentUserId={user?.userId ?? null}
|
||||
campaignId={campaignId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.grid}>
|
||||
{characters.length === 0 && (
|
||||
<p className={styles.empty}>
|
||||
|
|
@ -419,6 +456,7 @@ export default function CampaignView() {
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedCharacter && (
|
||||
<CharacterDetail
|
||||
|
|
@ -445,6 +483,13 @@ export default function CampaignView() {
|
|||
onClose={() => setShowCreate(false)}
|
||||
/>
|
||||
)}
|
||||
{showCombatStart && (
|
||||
<CombatStartModal
|
||||
characters={characters}
|
||||
campaignId={campaignId}
|
||||
onClose={() => setShowCombatStart(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} onUndoRoll={handleUndoRoll} />
|
||||
<DiceTray roll={diceRoll} onAnimationComplete={handleDiceComplete} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue