diff --git a/ts/Permission.ts b/ts/Permission.ts index da2129b..f678c1f 100644 --- a/ts/Permission.ts +++ b/ts/Permission.ts @@ -1,3 +1,36 @@ export interface Permission { values: boolean[]; } + +export const permissionLabels = [ + 'Build', + 'Demolish', + 'Industries and Facilities', + 'Purchase', + 'Switches', + 'Vehicles', + 'Remove Vegetation', + 'Rerail', +] as const satisfies ReadonlyArray; + +export function permissionToString(value?: Permission) { + if (typeof value === 'undefined') return 'undefined'; + const {values} = value; + if (values.every(Boolean)) return 'all'; + if (!values.some(Boolean)) return 'none'; + return values + .map((v, i) => [v, i < permissionLabels.length ? permissionLabels[i] : `Unknown permission ${i}`]) + .filter(([v]) => v) + .map(([, l]) => l) + .join(', '); +} + +export function permissionEqual( + a: Permission | undefined, + b: Permission | undefined, +): boolean { + if (typeof a === 'undefined') return typeof b === 'undefined'; + if (typeof b === 'undefined') return false; + if (a.values.length !== b.values.length) return false; + return a.values.every((v, i) => v === b.values[i]); +} diff --git a/ts/Studio.ts b/ts/Studio.ts index 898b2b7..b32ed30 100644 --- a/ts/Studio.ts +++ b/ts/Studio.ts @@ -2,6 +2,7 @@ import {createFilter} from './Filter'; import {calculateSteepestGrade} from './Grade'; import {GvasString, GvasText, gvasToString} from './Gvas'; import {createPager} from './Pager'; +import {Permission} from './Permission'; import {Frame, NumericFrameState, Railroad, SplineType, Quadruplet} from './Railroad'; import {MapLayers, RailroadMap} from './RailroadMap'; import {Rotator} from './Rotator'; @@ -13,6 +14,7 @@ import { editIndustryProducts, editIndustryType, editNumber, + editPermissions, editQuaternion, editRotator, editSlider, @@ -1533,7 +1535,10 @@ export class Studio { table.appendChild(thead); let tr = document.createElement('tr'); thead.appendChild(tr); - for (const columnHeader of ['Steam ID', 'Name', 'Money', 'XP', 'Location']) { + const hasPermissions = this.railroad.players.some((player) => typeof player.permissions !== 'undefined'); + const columnHeaders = ['Steam ID', 'Name', 'Money', 'XP', 'Location']; + if (hasPermissions) columnHeaders.push('Permissions'); + for (const columnHeader of columnHeaders) { const th = document.createElement('th'); th.textContent = columnHeader; tr.appendChild(th); @@ -1573,6 +1578,13 @@ export class Studio { td.replaceChildren(editNumber(this, player.rotation, {min: '-180', max: '180'}, setPlayerRotation)); tr.appendChild(td); } + // Permissions + if (hasPermissions) { + td = document.createElement('td'); + const setPlayerPermissions = (p: Permission | undefined) => player.permissions = p; + td.appendChild(editPermissions(this, player.permissions, setPlayerPermissions)); + tr.appendChild(td); + } } } diff --git a/ts/StudioEditor.ts b/ts/StudioEditor.ts index 5f4c07a..379f115 100644 --- a/ts/StudioEditor.ts +++ b/ts/StudioEditor.ts @@ -1,5 +1,6 @@ import {GvasString, GvasText, gvasToString} from './Gvas'; import {IndustryName, IndustryNames, IndustryType, industryNames, isIndustryName} from './industries'; +import {Permission, permissionEqual, permissionLabels, permissionToString} from './Permission'; import {Quaternion} from './Quaternion'; import {Quadruplet} from './Railroad'; import {Rotator} from './Rotator'; @@ -111,6 +112,76 @@ export function editNumber( return pre; } +export function editPermissions( + studio: Studio, + value: Permission | undefined, + saveValue: (value: Permission | undefined) => Permission | undefined, +) { + const formatValue = () => permissionToString(value); + const vstack = document.createElement('div'); + vstack.classList.add('vstack', 'gap-2'); + const inputs: HTMLInputElement[] = []; + const fromValue: number = value?.values.length ?? 0; + const fromKnown = permissionLabels.length; + for (let i = 0; i < Math.max(fromValue, fromKnown); i++) { + const hstack = document.createElement('div'); + hstack.classList.add('hstack', 'gap-2'); + const input = document.createElement('input'); + input.id = `permission${i}-${Math.random().toString(36).substr(2, 16)}`; + input.type = 'checkbox'; + input.title = i < permissionLabels.length ? permissionLabels[i] : `Unknown permission ${i}`; + input.checked = typeof value !== 'undefined' && value.values[i]; + input.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + save(); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancel(); + } + }); + inputs.push(input); + const label = document.createElement('label'); + label.setAttribute('for', input.id); + label.innerText = input.title; + hstack.replaceChildren(input, label); + vstack.appendChild(hstack); + } + const getValue = (): Permission | undefined => { + const values = inputs.map((input) => input.checked); + return {values}; + }; + const onSave = () => { + value = getValue(); + value = saveValue(value); + return onCancel(); + }; + const onCancel = () => { + const inputValue = getValue(); + if (inputValue !== value) { + // Restore the original value + inputs.forEach((input, i) => { + input.checked = typeof value !== 'undefined' && value.values[i]; + }); + if (permissionEqual(inputValue, getValue())) { + // No effect. Close the edit control + return false; + } + return true; + } + // Close the edit control + return false; + }; + const preview = document.createElement('pre'); + preview.classList.add('mb-0'); + preview.textContent = formatValue(); + const form = document.createElement('form'); + form.classList.add('form-group', 'w-100'); + form.replaceChildren(vstack); + const [pre, save, cancel] = saveContext(studio, form, onSave, onCancel, formatValue); + return pre; +} + export function editSlider( studio: Studio, value: number,