Add escape-confirm disconnect and option-list effect picker
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.20 R63";
|
window.CHGRID_WEB_VERSION = "2026.02.20 R64";
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ const EDITABLE_ITEM_PROPERTY_KEYS = new Set([
|
|||||||
'sides',
|
'sides',
|
||||||
'number',
|
'number',
|
||||||
]);
|
]);
|
||||||
|
const OPTION_ITEM_PROPERTY_VALUES: Partial<Record<string, string[]>> = {
|
||||||
|
effect: EFFECT_SEQUENCE.map((effect) => effect.id),
|
||||||
|
};
|
||||||
const APP_BASE_URL = import.meta.env.BASE_URL || '/';
|
const APP_BASE_URL = import.meta.env.BASE_URL || '/';
|
||||||
function withBase(path: string): string {
|
function withBase(path: string): string {
|
||||||
const normalizedBase = APP_BASE_URL.endsWith('/') ? APP_BASE_URL : `${APP_BASE_URL}/`;
|
const normalizedBase = APP_BASE_URL.endsWith('/') ? APP_BASE_URL : `${APP_BASE_URL}/`;
|
||||||
@@ -145,6 +148,7 @@ const sharedRadioSources = new Map<string, SharedRadioSource>();
|
|||||||
const itemRadioOutputs = new Map<string, ItemRadioOutput>();
|
const itemRadioOutputs = new Map<string, ItemRadioOutput>();
|
||||||
let replaceTextOnNextType = false;
|
let replaceTextOnNextType = false;
|
||||||
let itemPropertiesReadOnly = false;
|
let itemPropertiesReadOnly = false;
|
||||||
|
let pendingEscapeDisconnect = false;
|
||||||
|
|
||||||
const signalingProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const signalingProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
const signalingUrl = `${signalingProtocol}://${window.location.host}/ws`;
|
const signalingUrl = `${signalingProtocol}://${window.location.host}/ws`;
|
||||||
@@ -362,6 +366,9 @@ function beginItemProperties(item: WorldItem, readOnly = false): void {
|
|||||||
state.selectedItemId = item.id;
|
state.selectedItemId = item.id;
|
||||||
state.mode = 'itemProperties';
|
state.mode = 'itemProperties';
|
||||||
itemPropertiesReadOnly = readOnly;
|
itemPropertiesReadOnly = readOnly;
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
state.itemPropertyKeys = getInspectItemPropertyKeys(item);
|
state.itemPropertyKeys = getInspectItemPropertyKeys(item);
|
||||||
} else {
|
} else {
|
||||||
@@ -383,6 +390,21 @@ function useItem(item: WorldItem): void {
|
|||||||
signaling.send({ type: 'item_use', itemId: item.id });
|
signaling.send({ type: 'item_use', itemId: item.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openItemPropertyOptionSelect(item: WorldItem, key: string): void {
|
||||||
|
const options = OPTION_ITEM_PROPERTY_VALUES[key];
|
||||||
|
if (!options || options.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.mode = 'itemPropertyOptionSelect';
|
||||||
|
state.editingPropertyKey = key;
|
||||||
|
state.itemPropertyOptionValues = options;
|
||||||
|
const currentValue = getItemPropertyValue(item, key);
|
||||||
|
const currentIndex = options.indexOf(currentValue);
|
||||||
|
state.itemPropertyOptionIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||||
|
updateStatus(`Select ${key}: ${state.itemPropertyOptionValues[state.itemPropertyOptionIndex]}`);
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
}
|
||||||
|
|
||||||
function releaseSharedRadioSource(streamUrl: string): void {
|
function releaseSharedRadioSource(streamUrl: string): void {
|
||||||
const shared = sharedRadioSources.get(streamUrl);
|
const shared = sharedRadioSources.get(streamUrl);
|
||||||
if (!shared) return;
|
if (!shared) return;
|
||||||
@@ -824,6 +846,10 @@ function disconnect(): void {
|
|||||||
state.itemPropertyKeys = [];
|
state.itemPropertyKeys = [];
|
||||||
state.itemPropertyIndex = 0;
|
state.itemPropertyIndex = 0;
|
||||||
state.editingPropertyKey = null;
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
|
itemPropertiesReadOnly = false;
|
||||||
|
pendingEscapeDisconnect = false;
|
||||||
|
|
||||||
connecting = false;
|
connecting = false;
|
||||||
dom.nicknameContainer.classList.remove('hidden');
|
dom.nicknameContainer.classList.remove('hidden');
|
||||||
@@ -1016,6 +1042,10 @@ function toggleMute(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleNormalModeInput(code: string, shiftKey: boolean): void {
|
function handleNormalModeInput(code: string, shiftKey: boolean): void {
|
||||||
|
if (code !== 'Escape' && pendingEscapeDisconnect) {
|
||||||
|
pendingEscapeDisconnect = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (code === 'KeyN') {
|
if (code === 'KeyN') {
|
||||||
state.mode = 'nickname';
|
state.mode = 'nickname';
|
||||||
state.nicknameInput = state.player.nickname;
|
state.nicknameInput = state.player.nickname;
|
||||||
@@ -1303,7 +1333,15 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (code === 'Escape') {
|
if (code === 'Escape') {
|
||||||
disconnect();
|
if (pendingEscapeDisconnect) {
|
||||||
|
pendingEscapeDisconnect = false;
|
||||||
|
disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingEscapeDisconnect = true;
|
||||||
|
updateStatus('Press Escape again to disconnect.');
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1519,12 +1557,18 @@ function handleItemPropertiesModeInput(code: string): void {
|
|||||||
if (!itemId) {
|
if (!itemId) {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
itemPropertiesReadOnly = false;
|
itemPropertiesReadOnly = false;
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const item = state.items.get(itemId);
|
const item = state.items.get(itemId);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
state.mode = 'normal';
|
state.mode = 'normal';
|
||||||
itemPropertiesReadOnly = false;
|
itemPropertiesReadOnly = false;
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
updateStatus('Item no longer exists.');
|
updateStatus('Item no longer exists.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
@@ -1554,6 +1598,10 @@ function handleItemPropertiesModeInput(code: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (OPTION_ITEM_PROPERTY_VALUES[key]) {
|
||||||
|
openItemPropertyOptionSelect(item, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.mode = 'itemPropertyEdit';
|
state.mode = 'itemPropertyEdit';
|
||||||
state.editingPropertyKey = key;
|
state.editingPropertyKey = key;
|
||||||
state.nicknameInput =
|
state.nicknameInput =
|
||||||
@@ -1576,6 +1624,9 @@ function handleItemPropertiesModeInput(code: string): void {
|
|||||||
state.selectedItemId = null;
|
state.selectedItemId = null;
|
||||||
state.itemPropertyKeys = [];
|
state.itemPropertyKeys = [];
|
||||||
state.itemPropertyIndex = 0;
|
state.itemPropertyIndex = 0;
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
updateStatus('Closed item properties.');
|
updateStatus('Closed item properties.');
|
||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
}
|
}
|
||||||
@@ -1684,6 +1735,47 @@ function handleItemPropertyEditModeInput(code: string, key: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleItemPropertyOptionSelectModeInput(code: string): void {
|
||||||
|
const itemId = state.selectedItemId;
|
||||||
|
const propertyKey = state.editingPropertyKey;
|
||||||
|
if (!itemId || !propertyKey || state.itemPropertyOptionValues.length === 0) {
|
||||||
|
state.mode = 'itemProperties';
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'ArrowDown' || code === 'ArrowUp') {
|
||||||
|
state.itemPropertyOptionIndex =
|
||||||
|
code === 'ArrowDown'
|
||||||
|
? (state.itemPropertyOptionIndex + 1) % state.itemPropertyOptionValues.length
|
||||||
|
: (state.itemPropertyOptionIndex - 1 + state.itemPropertyOptionValues.length) % state.itemPropertyOptionValues.length;
|
||||||
|
updateStatus(`${propertyKey}: ${state.itemPropertyOptionValues[state.itemPropertyOptionIndex]}`);
|
||||||
|
audio.sfxUiBlip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'Enter') {
|
||||||
|
const selectedValue = state.itemPropertyOptionValues[state.itemPropertyOptionIndex];
|
||||||
|
signaling.send({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
||||||
|
state.mode = 'itemProperties';
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === 'Escape') {
|
||||||
|
state.mode = 'itemProperties';
|
||||||
|
state.editingPropertyKey = null;
|
||||||
|
state.itemPropertyOptionValues = [];
|
||||||
|
state.itemPropertyOptionIndex = 0;
|
||||||
|
updateStatus('Cancelled.');
|
||||||
|
audio.sfxUiCancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleNicknameModeInput(code: string, key: string): void {
|
function handleNicknameModeInput(code: string, key: string): void {
|
||||||
if (code === 'Enter') {
|
if (code === 'Enter') {
|
||||||
const clean = sanitizeName(state.nicknameInput);
|
const clean = sanitizeName(state.nicknameInput);
|
||||||
@@ -1778,6 +1870,8 @@ function setupInputHandlers(): void {
|
|||||||
handleItemPropertiesModeInput(code);
|
handleItemPropertiesModeInput(code);
|
||||||
} else if (state.mode === 'itemPropertyEdit') {
|
} else if (state.mode === 'itemPropertyEdit') {
|
||||||
handleItemPropertyEditModeInput(code, event.key);
|
handleItemPropertyEditModeInput(code, event.key);
|
||||||
|
} else if (state.mode === 'itemPropertyOptionSelect') {
|
||||||
|
handleItemPropertyOptionSelectModeInput(code);
|
||||||
} else {
|
} else {
|
||||||
handleNormalModeInput(code, event.shiftKey);
|
handleNormalModeInput(code, event.shiftKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export type GameMode =
|
|||||||
| 'addItem'
|
| 'addItem'
|
||||||
| 'selectItem'
|
| 'selectItem'
|
||||||
| 'itemProperties'
|
| 'itemProperties'
|
||||||
| 'itemPropertyEdit';
|
| 'itemPropertyEdit'
|
||||||
|
| 'itemPropertyOptionSelect';
|
||||||
|
|
||||||
export type Player = {
|
export type Player = {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
@@ -66,6 +67,8 @@ export type GameState = {
|
|||||||
itemPropertyKeys: string[];
|
itemPropertyKeys: string[];
|
||||||
itemPropertyIndex: number;
|
itemPropertyIndex: number;
|
||||||
editingPropertyKey: string | null;
|
editingPropertyKey: string | null;
|
||||||
|
itemPropertyOptionValues: string[];
|
||||||
|
itemPropertyOptionIndex: number;
|
||||||
addItemTypeIndex: number;
|
addItemTypeIndex: number;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
player: Player;
|
player: Player;
|
||||||
@@ -93,6 +96,8 @@ export function createInitialState(): GameState {
|
|||||||
itemPropertyKeys: [],
|
itemPropertyKeys: [],
|
||||||
itemPropertyIndex: 0,
|
itemPropertyIndex: 0,
|
||||||
editingPropertyKey: null,
|
editingPropertyKey: null,
|
||||||
|
itemPropertyOptionValues: [],
|
||||||
|
itemPropertyOptionIndex: 0,
|
||||||
addItemTypeIndex: 0,
|
addItemTypeIndex: 0,
|
||||||
isMuted: false,
|
isMuted: false,
|
||||||
player: {
|
player: {
|
||||||
|
|||||||
Reference in New Issue
Block a user