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 = {
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<void> {
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<void> {
}
state.selectedCampaign = await getCampaign(state.selectedCampaignId);
syncSelectedCharacter();
}
async function reloadCampaignLog(): Promise<void> {
@@ -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) => `<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("");
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) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`)
.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;
}

View File

@@ -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) => `<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("");
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) => `<option value="${skill.id}">${skill.name} (${skill.diceRollDefinition})</option>`)
.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;
}