diff --git a/RpgRoller/frontend/app.ts b/RpgRoller/frontend/app.ts index a1e30da..ba23372 100644 --- a/RpgRoller/frontend/app.ts +++ b/RpgRoller/frontend/app.ts @@ -61,6 +61,8 @@ const rollVisibilitySelect = mustSelect("roll-visibility"); type AppState = { user: UserSummary | null; + activeCharacterId: string | null; + selectedCharacterId: string | null; campaigns: CampaignSummary[]; selectedCampaignId: string | null; selectedCampaign: CampaignDetails | null; @@ -71,6 +73,8 @@ type AppState = { const state: AppState = { user: null, + activeCharacterId: null, + selectedCharacterId: null, campaigns: [], selectedCampaignId: null, selectedCampaign: null, @@ -138,11 +142,21 @@ campaignSelect.addEventListener("change", async () => { const selected = campaignSelect.value; state.selectedCampaignId = selected.length > 0 ? selected : null; await reloadSelectedCampaign(); + syncSelectedCharacter(); + renderCharacterSelect(); + renderSkillSelect(); connectStateEvents(); - renderAll(); + renderCampaignMeta(); + renderCampaignDetails(); + renderCampaignLog(); }); }); +characterSelect.addEventListener("change", () => { + state.selectedCharacterId = characterSelect.value.length > 0 ? characterSelect.value : null; + renderSkillSelect(); +}); + refreshCampaignButton.addEventListener("click", async () => { await runAction(async () => { await reloadSelectedCampaign(); @@ -179,6 +193,8 @@ activateCharacterButton.addEventListener("click", async () => { } await activateCharacter(characterId); + state.activeCharacterId = characterId; + state.selectedCharacterId = characterId; await reloadAll(); setMessage("Active character updated.", false); }); @@ -270,10 +286,12 @@ async function reloadSession(): Promise { try { const me = await getMe(); state.user = me.user; + state.activeCharacterId = me.activeCharacterId ?? null; state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId; } catch { state.user = null; + state.activeCharacterId = null; } } @@ -317,6 +335,7 @@ async function reloadSelectedCampaign(): Promise { } state.selectedCampaign = await getCampaign(state.selectedCampaignId); + syncSelectedCharacter(); } async function reloadCampaignLog(): Promise { @@ -368,6 +387,8 @@ function resetStateAfterLogout(): void { } function resetAuthenticatedState(): void { + state.activeCharacterId = null; + state.selectedCharacterId = null; state.campaigns = []; state.selectedCampaignId = null; state.selectedCampaign = null; @@ -404,7 +425,9 @@ function renderCampaignMeta(): void { } const isGm = state.selectedCampaign.gm.id === state.user.id; - campaignMetaElement.textContent = `Selected: ${state.selectedCampaign.name} (${state.selectedCampaign.rulesetId}) - ${isGm ? "You are GM" : "Player context"}`; + const activeCharacter = state.selectedCampaign.characters.find((character) => character.id === state.activeCharacterId); + const activeLabel = activeCharacter ? ` | Active: ${activeCharacter.name}` : ""; + campaignMetaElement.textContent = `Selected: ${state.selectedCampaign.name} (${state.selectedCampaign.rulesetId}) - ${isGm ? "You are GM" : "Player context"}${activeLabel}`; } function renderCampaignDetails(): void { @@ -433,14 +456,21 @@ function renderCampaignDetails(): void { function renderCharacterSelect(): void { if (!state.selectedCampaign) { characterSelect.innerHTML = ""; + state.selectedCharacterId = null; return; } + const selectedCharacterId = resolveSelectedCharacterId(); + const options = state.selectedCampaign.characters - .map((character) => ``) + .map((character) => { + const selected = character.id === selectedCharacterId ? " selected" : ""; + return ``; + }) .join(""); characterSelect.innerHTML = options; + state.selectedCharacterId = selectedCharacterId; } function renderSkillSelect(): void { @@ -449,7 +479,9 @@ function renderSkillSelect(): void { return; } - const options = state.selectedCampaign.skills + const selectedCharacterId = resolveSelectedCharacterId(); + const characterSkills = state.selectedCampaign.skills.filter((skill) => skill.characterId === selectedCharacterId); + const options = characterSkills .map((skill) => ``) .join(""); @@ -467,8 +499,11 @@ function selectedSkillFromCampaign(): SkillSummary | null { return null; } + const selectedCharacterId = resolveSelectedCharacterId(); const selectedSkillId = skillSelect.value; - return state.selectedCampaign.skills.find((skill) => skill.id === selectedSkillId) ?? null; + return state.selectedCampaign.skills + .filter((skill) => skill.characterId === selectedCharacterId) + .find((skill) => skill.id === selectedSkillId) ?? null; } function renderCampaignLog(): void { @@ -554,3 +589,33 @@ function mustButton(id: string): HTMLButtonElement { return element; } + +function syncSelectedCharacter(): void { + if (!state.selectedCampaign) { + state.selectedCharacterId = null; + return; + } + + const availableIds = new Set(state.selectedCampaign.characters.map((character) => character.id)); + if (state.selectedCharacterId && availableIds.has(state.selectedCharacterId)) { + return; + } + + if (state.activeCharacterId && availableIds.has(state.activeCharacterId)) { + state.selectedCharacterId = state.activeCharacterId; + return; + } + + state.selectedCharacterId = state.selectedCampaign.characters.length > 0 + ? state.selectedCampaign.characters[0].id + : null; +} + +function resolveSelectedCharacterId(): string | null { + if (!state.selectedCampaign) { + return null; + } + + syncSelectedCharacter(); + return state.selectedCharacterId; +} diff --git a/RpgRoller/wwwroot/app.js b/RpgRoller/wwwroot/app.js index ebd04c4..8440021 100644 --- a/RpgRoller/wwwroot/app.js +++ b/RpgRoller/wwwroot/app.js @@ -31,6 +31,8 @@ const rollForm = mustForm("roll-form"); const rollVisibilitySelect = mustSelect("roll-visibility"); const state = { user: null, + activeCharacterId: null, + selectedCharacterId: null, campaigns: [], selectedCampaignId: null, selectedCampaign: null, @@ -87,10 +89,19 @@ campaignSelect.addEventListener("change", async () => { const selected = campaignSelect.value; state.selectedCampaignId = selected.length > 0 ? selected : null; await reloadSelectedCampaign(); + syncSelectedCharacter(); + renderCharacterSelect(); + renderSkillSelect(); connectStateEvents(); - renderAll(); + renderCampaignMeta(); + renderCampaignDetails(); + renderCampaignLog(); }); }); +characterSelect.addEventListener("change", () => { + state.selectedCharacterId = characterSelect.value.length > 0 ? characterSelect.value : null; + renderSkillSelect(); +}); refreshCampaignButton.addEventListener("click", async () => { await runAction(async () => { await reloadSelectedCampaign(); @@ -121,6 +132,8 @@ activateCharacterButton.addEventListener("click", async () => { throw new Error("Select a character to activate."); } await activateCharacter(characterId); + state.activeCharacterId = characterId; + state.selectedCharacterId = characterId; await reloadAll(); setMessage("Active character updated.", false); }); @@ -196,10 +209,12 @@ async function reloadSession() { try { const me = await getMe(); state.user = me.user; + state.activeCharacterId = me.activeCharacterId ?? null; state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId; } catch { state.user = null; + state.activeCharacterId = null; } } async function ensureRulesets() { @@ -233,6 +248,7 @@ async function reloadSelectedCampaign() { return; } state.selectedCampaign = await getCampaign(state.selectedCampaignId); + syncSelectedCharacter(); } async function reloadCampaignLog() { if (!state.selectedCampaignId) { @@ -274,6 +290,8 @@ function resetStateAfterLogout() { closeStateEvents(); } function resetAuthenticatedState() { + state.activeCharacterId = null; + state.selectedCharacterId = null; state.campaigns = []; state.selectedCampaignId = null; state.selectedCampaign = null; @@ -305,7 +323,9 @@ function renderCampaignMeta() { return; } const isGm = state.selectedCampaign.gm.id === state.user.id; - campaignMetaElement.textContent = `Selected: ${state.selectedCampaign.name} (${state.selectedCampaign.rulesetId}) - ${isGm ? "You are GM" : "Player context"}`; + const activeCharacter = state.selectedCampaign.characters.find((character) => character.id === state.activeCharacterId); + const activeLabel = activeCharacter ? ` | Active: ${activeCharacter.name}` : ""; + campaignMetaElement.textContent = `Selected: ${state.selectedCampaign.name} (${state.selectedCampaign.rulesetId}) - ${isGm ? "You are GM" : "Player context"}${activeLabel}`; } function renderCampaignDetails() { if (!state.selectedCampaign) { @@ -329,19 +349,27 @@ function renderCampaignDetails() { function renderCharacterSelect() { if (!state.selectedCampaign) { characterSelect.innerHTML = ""; + state.selectedCharacterId = null; return; } + const selectedCharacterId = resolveSelectedCharacterId(); const options = state.selectedCampaign.characters - .map((character) => ``) + .map((character) => { + const selected = character.id === selectedCharacterId ? " selected" : ""; + return ``; + }) .join(""); characterSelect.innerHTML = options; + state.selectedCharacterId = selectedCharacterId; } function renderSkillSelect() { if (!state.selectedCampaign) { skillSelect.innerHTML = ""; return; } - const options = state.selectedCampaign.skills + const selectedCharacterId = resolveSelectedCharacterId(); + const characterSkills = state.selectedCampaign.skills.filter((skill) => skill.characterId === selectedCharacterId); + const options = characterSkills .map((skill) => ``) .join(""); skillSelect.innerHTML = options; @@ -355,8 +383,11 @@ function selectedSkillFromCampaign() { if (!state.selectedCampaign) { return null; } + const selectedCharacterId = resolveSelectedCharacterId(); const selectedSkillId = skillSelect.value; - return state.selectedCampaign.skills.find((skill) => skill.id === selectedSkillId) ?? null; + return state.selectedCampaign.skills + .filter((skill) => skill.characterId === selectedCharacterId) + .find((skill) => skill.id === selectedSkillId) ?? null; } function renderCampaignLog() { if (state.campaignLog.length === 0) { @@ -426,3 +457,27 @@ function mustButton(id) { } return element; } +function syncSelectedCharacter() { + if (!state.selectedCampaign) { + state.selectedCharacterId = null; + return; + } + const availableIds = new Set(state.selectedCampaign.characters.map((character) => character.id)); + if (state.selectedCharacterId && availableIds.has(state.selectedCharacterId)) { + return; + } + if (state.activeCharacterId && availableIds.has(state.activeCharacterId)) { + state.selectedCharacterId = state.activeCharacterId; + return; + } + state.selectedCharacterId = state.selectedCampaign.characters.length > 0 + ? state.selectedCampaign.characters[0].id + : null; +} +function resolveSelectedCharacterId() { + if (!state.selectedCampaign) { + return null; + } + syncSelectedCharacter(); + return state.selectedCharacterId; +}