diff --git a/package-lock.json b/package-lock.json index 8ddb108..e125626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -554,17 +554,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3580,9 +3569,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -5323,13 +5312,12 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "peer": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -5338,7 +5326,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", 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, diff --git a/ts/frames.ts b/ts/frames.ts index 933403c..2da665c 100644 --- a/ts/frames.ts +++ b/ts/frames.ts @@ -1699,6 +1699,7 @@ export const cargoLimits = { }, VentilatedBoxcarCC: { ['EFreightType::Meat']: 36, + ['EFreightType::None']: 0, }, } as const satisfies PRO>;