Preserve selected character when activating

This commit is contained in:
2026-02-24 22:34:14 +01:00
parent 4920618337
commit fb70c18e75
2 changed files with 130 additions and 10 deletions

View File

@@ -61,6 +61,8 @@ const rollVisibilitySelect = mustSelect("roll-visibility");
type AppState = { type AppState = {
user: UserSummary | null; user: UserSummary | null;
activeCharacterId: string | null;
selectedCharacterId: string | null;
campaigns: CampaignSummary[]; campaigns: CampaignSummary[];
selectedCampaignId: string | null; selectedCampaignId: string | null;
selectedCampaign: CampaignDetails | null; selectedCampaign: CampaignDetails | null;
@@ -71,6 +73,8 @@ type AppState = {
const state: AppState = { const state: AppState = {
user: null, user: null,
activeCharacterId: null,
selectedCharacterId: null,
campaigns: [], campaigns: [],
selectedCampaignId: null, selectedCampaignId: null,
selectedCampaign: null, selectedCampaign: null,
@@ -138,11 +142,21 @@ campaignSelect.addEventListener("change", async () => {
const selected = campaignSelect.value; const selected = campaignSelect.value;
state.selectedCampaignId = selected.length > 0 ? selected : null; state.selectedCampaignId = selected.length > 0 ? selected : null;
await reloadSelectedCampaign(); await reloadSelectedCampaign();
syncSelectedCharacter();
renderCharacterSelect();
renderSkillSelect();
connectStateEvents(); connectStateEvents();
renderAll(); renderCampaignMeta();
renderCampaignDetails();
renderCampaignLog();
}); });
}); });
characterSelect.addEventListener("change", () => {
state.selectedCharacterId = characterSelect.value.length > 0 ? characterSelect.value : null;
renderSkillSelect();
});
refreshCampaignButton.addEventListener("click", async () => { refreshCampaignButton.addEventListener("click", async () => {
await runAction(async () => { await runAction(async () => {
await reloadSelectedCampaign(); await reloadSelectedCampaign();
@@ -179,6 +193,8 @@ activateCharacterButton.addEventListener("click", async () => {
} }
await activateCharacter(characterId); await activateCharacter(characterId);
state.activeCharacterId = characterId;
state.selectedCharacterId = characterId;
await reloadAll(); await reloadAll();
setMessage("Active character updated.", false); setMessage("Active character updated.", false);
}); });
@@ -270,10 +286,12 @@ async function reloadSession(): Promise<void> {
try { try {
const me = await getMe(); const me = await getMe();
state.user = me.user; state.user = me.user;
state.activeCharacterId = me.activeCharacterId ?? null;
state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId; state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId;
} }
catch { catch {
state.user = null; state.user = null;
state.activeCharacterId = null;
} }
} }
@@ -317,6 +335,7 @@ async function reloadSelectedCampaign(): Promise<void> {
} }
state.selectedCampaign = await getCampaign(state.selectedCampaignId); state.selectedCampaign = await getCampaign(state.selectedCampaignId);
syncSelectedCharacter();
} }
async function reloadCampaignLog(): Promise<void> { async function reloadCampaignLog(): Promise<void> {
@@ -368,6 +387,8 @@ function resetStateAfterLogout(): void {
} }
function resetAuthenticatedState(): void { function resetAuthenticatedState(): void {
state.activeCharacterId = null;
state.selectedCharacterId = null;
state.campaigns = []; state.campaigns = [];
state.selectedCampaignId = null; state.selectedCampaignId = null;
state.selectedCampaign = null; state.selectedCampaign = null;
@@ -404,7 +425,9 @@ function renderCampaignMeta(): void {
} }
const isGm = state.selectedCampaign.gm.id === state.user.id; 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 { function renderCampaignDetails(): void {
@@ -433,14 +456,21 @@ function renderCampaignDetails(): void {
function renderCharacterSelect(): void { function renderCharacterSelect(): void {
if (!state.selectedCampaign) { if (!state.selectedCampaign) {
characterSelect.innerHTML = ""; characterSelect.innerHTML = "";
state.selectedCharacterId = null;
return; return;
} }
const selectedCharacterId = resolveSelectedCharacterId();
const options = state.selectedCampaign.characters const options = state.selectedCampaign.characters
.map((character) => `<option value="${character.id}">${character.name}</option>`) .map((character) => {
const selected = character.id === selectedCharacterId ? " selected" : "";
return `<option value="${character.id}"${selected}>${character.name}</option>`;
})
.join(""); .join("");
characterSelect.innerHTML = options; characterSelect.innerHTML = options;
state.selectedCharacterId = selectedCharacterId;
} }
function renderSkillSelect(): void { function renderSkillSelect(): void {
@@ -449,7 +479,9 @@ function renderSkillSelect(): void {
return; return;
} }
const options = state.selectedCampaign.skills const selectedCharacterId = resolveSelectedCharacterId();
const characterSkills = state.selectedCampaign.skills.filter((skill) => skill.characterId === selectedCharacterId);
const options = characterSkills
.map((skill) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`) .map((skill) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`)
.join(""); .join("");
@@ -467,8 +499,11 @@ function selectedSkillFromCampaign(): SkillSummary | null {
return null; return null;
} }
const selectedCharacterId = resolveSelectedCharacterId();
const selectedSkillId = skillSelect.value; 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 { function renderCampaignLog(): void {
@@ -554,3 +589,33 @@ function mustButton(id: string): HTMLButtonElement {
return element; 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;
}

View File

@@ -31,6 +31,8 @@ const rollForm = mustForm("roll-form");
const rollVisibilitySelect = mustSelect("roll-visibility"); const rollVisibilitySelect = mustSelect("roll-visibility");
const state = { const state = {
user: null, user: null,
activeCharacterId: null,
selectedCharacterId: null,
campaigns: [], campaigns: [],
selectedCampaignId: null, selectedCampaignId: null,
selectedCampaign: null, selectedCampaign: null,
@@ -87,10 +89,19 @@ campaignSelect.addEventListener("change", async () => {
const selected = campaignSelect.value; const selected = campaignSelect.value;
state.selectedCampaignId = selected.length > 0 ? selected : null; state.selectedCampaignId = selected.length > 0 ? selected : null;
await reloadSelectedCampaign(); await reloadSelectedCampaign();
syncSelectedCharacter();
renderCharacterSelect();
renderSkillSelect();
connectStateEvents(); connectStateEvents();
renderAll(); renderCampaignMeta();
renderCampaignDetails();
renderCampaignLog();
}); });
}); });
characterSelect.addEventListener("change", () => {
state.selectedCharacterId = characterSelect.value.length > 0 ? characterSelect.value : null;
renderSkillSelect();
});
refreshCampaignButton.addEventListener("click", async () => { refreshCampaignButton.addEventListener("click", async () => {
await runAction(async () => { await runAction(async () => {
await reloadSelectedCampaign(); await reloadSelectedCampaign();
@@ -121,6 +132,8 @@ activateCharacterButton.addEventListener("click", async () => {
throw new Error("Select a character to activate."); throw new Error("Select a character to activate.");
} }
await activateCharacter(characterId); await activateCharacter(characterId);
state.activeCharacterId = characterId;
state.selectedCharacterId = characterId;
await reloadAll(); await reloadAll();
setMessage("Active character updated.", false); setMessage("Active character updated.", false);
}); });
@@ -196,10 +209,12 @@ async function reloadSession() {
try { try {
const me = await getMe(); const me = await getMe();
state.user = me.user; state.user = me.user;
state.activeCharacterId = me.activeCharacterId ?? null;
state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId; state.selectedCampaignId = me.currentCampaignId ?? state.selectedCampaignId;
} }
catch { catch {
state.user = null; state.user = null;
state.activeCharacterId = null;
} }
} }
async function ensureRulesets() { async function ensureRulesets() {
@@ -233,6 +248,7 @@ async function reloadSelectedCampaign() {
return; return;
} }
state.selectedCampaign = await getCampaign(state.selectedCampaignId); state.selectedCampaign = await getCampaign(state.selectedCampaignId);
syncSelectedCharacter();
} }
async function reloadCampaignLog() { async function reloadCampaignLog() {
if (!state.selectedCampaignId) { if (!state.selectedCampaignId) {
@@ -274,6 +290,8 @@ function resetStateAfterLogout() {
closeStateEvents(); closeStateEvents();
} }
function resetAuthenticatedState() { function resetAuthenticatedState() {
state.activeCharacterId = null;
state.selectedCharacterId = null;
state.campaigns = []; state.campaigns = [];
state.selectedCampaignId = null; state.selectedCampaignId = null;
state.selectedCampaign = null; state.selectedCampaign = null;
@@ -305,7 +323,9 @@ function renderCampaignMeta() {
return; return;
} }
const isGm = state.selectedCampaign.gm.id === state.user.id; 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() { function renderCampaignDetails() {
if (!state.selectedCampaign) { if (!state.selectedCampaign) {
@@ -329,19 +349,27 @@ function renderCampaignDetails() {
function renderCharacterSelect() { function renderCharacterSelect() {
if (!state.selectedCampaign) { if (!state.selectedCampaign) {
characterSelect.innerHTML = ""; characterSelect.innerHTML = "";
state.selectedCharacterId = null;
return; return;
} }
const selectedCharacterId = resolveSelectedCharacterId();
const options = state.selectedCampaign.characters const options = state.selectedCampaign.characters
.map((character) => `<option value="${character.id}">${character.name}</option>`) .map((character) => {
const selected = character.id === selectedCharacterId ? " selected" : "";
return `<option value="${character.id}"${selected}>${character.name}</option>`;
})
.join(""); .join("");
characterSelect.innerHTML = options; characterSelect.innerHTML = options;
state.selectedCharacterId = selectedCharacterId;
} }
function renderSkillSelect() { function renderSkillSelect() {
if (!state.selectedCampaign) { if (!state.selectedCampaign) {
skillSelect.innerHTML = ""; skillSelect.innerHTML = "";
return; return;
} }
const options = state.selectedCampaign.skills const selectedCharacterId = resolveSelectedCharacterId();
const characterSkills = state.selectedCampaign.skills.filter((skill) => skill.characterId === selectedCharacterId);
const options = characterSkills
.map((skill) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`) .map((skill) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`)
.join(""); .join("");
skillSelect.innerHTML = options; skillSelect.innerHTML = options;
@@ -355,8 +383,11 @@ function selectedSkillFromCampaign() {
if (!state.selectedCampaign) { if (!state.selectedCampaign) {
return null; return null;
} }
const selectedCharacterId = resolveSelectedCharacterId();
const selectedSkillId = skillSelect.value; 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() { function renderCampaignLog() {
if (state.campaignLog.length === 0) { if (state.campaignLog.length === 0) {
@@ -426,3 +457,27 @@ function mustButton(id) {
} }
return element; 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;
}