feat(slots UI): much better slot occupant card

This commit is contained in:
Rachel Powers
2024-04-22 22:45:21 -07:00
parent d240095020
commit 17907151b3
5 changed files with 241 additions and 591 deletions

View File

@@ -14,7 +14,8 @@ import type {
Pins,
} from "ic10emu_wasm";
import { structuralEqual } from "utils";
import { LitElement } from "lit";
import { LitElement, PropertyValueMap } from "lit";
import type { DeviceDB } from "./device_db";
type Constructor<T = {}> = new (...args: any[]) => T;
@@ -46,7 +47,7 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
class VMDeviceMixinClass extends superClass {
_deviceID: number;
private _deviceID: number;
get deviceID() {
return this._deviceID;
}
@@ -181,6 +182,11 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
this.pins = pins;
}
}
update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.update(changedProperties);
this.updateDevice();
}
}
return VMDeviceMixinClass as Constructor<VMDeviceMixinInterface> & T;
};
@@ -215,5 +221,42 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
this.updateDevice();
}
}
return VMActiveICMixinClass as Constructor<VMDeviceMixinInterface> & T;
};
export declare class VMDeviceDBMixinInterface {
deviceDB: DeviceDB;
_handleDeviceDBLoad(e: CustomEvent): void
}
export const VMDeviceDBMixin = <T extends Constructor<LitElement>>(superClass: T) => {
class VMDeviceDBMixinClass extends superClass {
connectedCallback(): void {
const root = super.connectedCallback();
window.VM.vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
return root;
}
_handleDeviceDBLoad(e: CustomEvent) {
this.deviceDB = e.detail;
}
private _deviceDB: DeviceDB;
get deviceDB(): DeviceDB {
return this._deviceDB;
}
@state()
set deviceDB(val: DeviceDB) {
this._deviceDB = val;
}
}
return VMDeviceDBMixinClass as Constructor<VMDeviceDBMixinInterface> & T
}

View File

@@ -1,458 +0,0 @@
import type {
LogicType,
Slot, SlotOccupant, SlotLogicType, SlotType
} from "ic10emu_wasm";
import { html, css, HTMLTemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceMixin } from "virtual_machine/base_device";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.js";
import {
displayNumber,
parseIntWithHexOrBinary,
parseNumber
} from "../../utils";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js";
import type { DeviceDB } from "virtual_machine/device_db";
import { SlDialog } from "@shoelace-style/shoelace";
@customElement("vm-device-card")
export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
image_err: boolean;
@property({ type: Boolean }) open: boolean;
constructor() {
super();
this.open = false;
}
static styles = [
...defaultCss,
css`
:host {
display: block;
box-sizing: border-box;
}
.card {
width: 100%;
box-sizing: border-box;
}
.image {
width: 4rem;
height: 4rem;
}
.header {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.header-name {
display: flex;
flex-direction: row;
width: 100%;
flex-grow: 1;
align-items: center;
flex-wrap: wrap;
}
.device-card {
--padding: var(--sl-spacing-small);
}
.device-name::part(input) {
width: 10rem;
}
.device-id::part(input) {
width: 7rem;
}
.device-name-hash::part(input) {
width: 7rem;
}
.slot-header.image {
width: 1.5rem;
height: 1.5rem;
border: var(--sl-panel-border-width) solid var(--sl-panel-border-color);
border-radius: var(--sl-border-radius-medium);
background-color: var(--sl-color-neutral-0);
}
sl-divider {
--spacing: 0.25rem;
}
sl-button[variant="success"] {
/* Changes the success theme color to purple using primitives */
--sl-color-success-600: var(--sl-color-purple-700);
}
sl-button[variant="primary"] {
/* Changes the success theme color to purple using primitives */
--sl-color-primary-600: var(--sl-color-cyan-600);
}
sl-button[variant="warning"] {
/* Changes the success theme color to purple using primitives */
--sl-color-warning-600: var(--sl-color-amber-600);
}
sl-tab-group {
margin-left: 1rem;
margin-right: 1rem;
--indicator-color: var(--sl-color-purple-600);
--sl-color-primary-600: var(--sl-color-purple-600);
}
sl-tab::part(base) {
padding: var(--sl-spacing-small) var(--sl-spacing-medium);
}
sl-tab-group::part(base) {
max-height: 20rem;
overflow-y: auto;
}
sl-icon-button.remove-button::part(base) {
color: var(--sl-color-danger-600);
}
sl-icon-button.remove-button::part(base):hover,
sl-icon-button.remove-button::part(base):focus {
color: var(--sl-color-danger-500);
}
sl-icon-button.remove-button::part(base):active {
color: var(--sl-color-danger-600);
}
.remove-dialog-body {
display: flex;
flex-direction: row;
}
.dialog-image {
width: 3rem;
height: 3rem;
}
`,
];
private _deviceDB: DeviceDB;
get deviceDB(): DeviceDB {
return this._deviceDB;
}
@state()
set deviceDB(val: DeviceDB) {
this._deviceDB = val;
this.updateDevice();
this.requestUpdate();
}
connectedCallback(): void {
super.connectedCallback();
window.VM.vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this)
);
}
_handleDeviceDBLoad(e: CustomEvent) {
this.deviceDB = e.detail;
}
onImageErr(e: Event) {
this.image_err = true;
console.log("Image load error", e);
}
renderHeader(): HTMLTemplateResult {
const activeIc = window.VM.vm.activeIC;
const thisIsActiveIc = activeIc.id === this.deviceID;
const badges: HTMLTemplateResult[] = [];
if (this.deviceID == activeIc?.id) {
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
activeIc?.pins?.forEach((id, index) => {
if (this.deviceID == id) {
badges.push(
html`<sl-badge variant="success" pill>d${index}</sl-badge>`
);
}
}, this);
return html`
<sl-tooltip content="${this.prefabName}">
<img class="image" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
</sl-tooltip>
<div class="header-name">
<sl-input id="vmDeviceCard${this.deviceID}Id" class="device-id" size="small" pill value=${this.deviceID}
@sl-change=${this._handleChangeID}>
<span slot="prefix">Id</span>
<sl-copy-button slot="suffix" value=${this.deviceID}></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}Name" class="device-name" size="small" pill placeholder=${this.prefabName}
value=${this.name} @sl-change=${this._handleChangeName}>
<span slot="prefix">Name</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}Name.value"></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash"
value="${this.nameHash}" disabled>
<span slot="prefix">Hash</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}NameHash.value"></sl-copy-button>
</sl-input>
${badges.map((badge) => badge)}
</div>
<div class="ms-auto mt-auto mb-auto me-2">
<sl-tooltip content=${thisIsActiveIc ? "Removing the selected Active IC is disabled" : "Remove Device"}>
<sl-icon-button class="remove-button" name="trash" label="Remove Device" ?disabled=${thisIsActiveIc}
@click=${this._handleDeviceRemoveButton}></sl-icon-button>
</sl-tooltip>
</div>
`;
}
renderFields(): HTMLTemplateResult {
const fields = Array.from(this.fields.entries());
const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
return html`
${fields.map(([name, field], _index, _fields) => {
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${displayNumber(field.value)}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`;
}
lookupSlotOccupantImg(
occupant: SlotOccupant | undefined,
typ: SlotType
): string {
if (typeof occupant !== "undefined") {
const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash";
return `img/stationpedia/${prefabName}.png`;
} else {
return `img/stationpedia/SlotIcon_${typ}.png`;
}
}
_onSlotImageErr(e: Event) {
console.log("image_err", e);
}
static transparentImg = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
const _fields = this.device.getSlotFields(slotIndex);
const fields = Array.from(_fields.entries());
const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`;
const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ);
return html`
<sl-card class="slot-card">
<img slot="header" class="slot-header image" src="${slotImg}" onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<span slot="header" class="slot-header">${slotIndex} : ${slot.typ}</span>
${typeof slot.occupant !== "undefined"
? html`
<span slot="header" class="slot-header">
Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash}
</span>
<span slot="header" class="slot-header">
Quantity: ${slot.occupant.quantity}/
${slot.occupant.max_quantity}
</span>
`
: ""}
<div class="slot-fields">
${fields.map(
([name, field], _index, _fields) => html`
<sl-input
id="${inputIdBase}${name}"
slotIndex=${slotIndex}
key="${name}"
value="${displayNumber(field.value)}"
size="small"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`
)}
</div>
</sl-card>
`;
}
renderSlots(): HTMLTemplateResult {
return html`
<div class="slots">
${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))}
</div>
`;
}
renderReagents(): HTMLTemplateResult {
return html``;
}
renderNetworks(): HTMLTemplateResult {
const vmNetworks = window.VM.vm.networks;
const networks = this.connections.map((connection, index, _conns) => {
const conn = typeof connection === "object" ? connection.CableNetwork : null;
return html`
<sl-select hoist placement="top" clearable key=${index} value=${conn?.net} ?disabled=${conn === null}
@sl-change=${this._handleChangeConnection}>
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) => html`<sl-option value=${net}>Network ${net}</sl-option>`
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
`;
});
return html`
<div class="networks">
${networks}
</div>
`;
}
renderPins(): HTMLTemplateResult {
const pins = this.pins;
const visibleDevices = window.VM.vm.visibleDevices(this.deviceID);
const pinsHtml = pins?.map(
(pin, index) => html`
<sl-select hoist placement="top" clearable key=${index} value=${pin} @sl-change=${this._handleChangePin}>
<span slot="prefix">d${index}</span>
${visibleDevices.map(
(device, _index) => html`
<sl-option value=${device.id}>
Device ${device.id} : ${device.name ?? device.prefabName}
</sl-option>
`
)}
</sl-select>`
);
return html`
<div class="pins" >
${pinsHtml}
</div>
`;
}
render(): HTMLTemplateResult {
return html`
<ic10-details class="device-card" ?open=${this.open}>
<div class="header" slot="summary">${this.renderHeader()}</div>
<sl-tab-group>
<sl-tab slot="nav" panel="fields" active>Fields</sl-tab>
<sl-tab slot="nav" panel="slots">Slots</sl-tab>
<sl-tab slot="nav" panel="reagents" disabled>Reagents</sl-tab>
<sl-tab slot="nav" panel="networks">Networks</sl-tab>
<sl-tab slot="nav" panel="pins" ?disabled=${!this.pins}>Pins</sl-tab>
<sl-tab-panel name="fields" active>${this.renderFields()}</sl-tab-panel>
<sl-tab-panel name="slots">${this.renderSlots()}</sl-tab-panel>
<sl-tab-panel name="reagents">${this.renderReagents()}</sl-tab-panel>
<sl-tab-panel name="networks">${this.renderNetworks()}</sl-tab-panel>
<sl-tab-panel name="pins">${this.renderPins()}</sl-tab-panel>
</sl-tab-group>
</ic10-details>
<sl-dialog class="remove-device-dialog" no-header @sl-request-close=${this._preventOverlayClose}>
<div class="remove-dialog-body">
<img class="dialog-image mt-auto mb-auto me-2" src="img/stationpedia/${this.prefabName}.png"
onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<div class="flex-g">
<p><strong>Are you sure you want to remove this device?</strong></p>
<span>Id ${this.deviceID} : ${this.name ?? this.prefabName}</span>
</div>
</div>
<div slot="footer">
<sl-button variant="primary" autofocus @click=${this._closeRemoveDialog}>Close</sl-button>
<sl-button variant="danger" @click=${this._removeDialogRemove}>Remove</sl-button>
</div>
</sl-dialog>
`;
}
@query(".remove-device-dialog") removeDialog: SlDialog;
_preventOverlayClose(event: CustomEvent) {
if (event.detail.source === "overlay") {
event.preventDefault();
}
}
_closeRemoveDialog() {
this.removeDialog.hide();
}
_handleChangeID(e: CustomEvent) {
const input = e.target as SlInput;
const val = parseIntWithHexOrBinary(input.value);
if (!isNaN(val)) {
window.VM.get().then(vm => {
if (!vm.changeDeviceId(this.deviceID, val)) {
input.value = this.deviceID.toString();
}
});
} else {
input.value = this.deviceID.toString();
}
}
_handleChangeName(e: CustomEvent) {
const input = e.target as SlInput;
const name = input.value.length === 0 ? undefined : input.value;
window.VM.get().then(vm => {
if (!vm.setDeviceName(this.deviceID, name)) {
input.value = this.name;
}
this.updateDevice();
});
}
_handleChangeField(e: CustomEvent) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceField(this.deviceID, field, val, true)) {
input.value = this.fields.get(field).value.toString();
}
this.updateDevice();
});
}
_handleChangeSlotField(e: CustomEvent) {
const input = e.target as SlInput;
const slot = parseInt(input.getAttribute("slotIndex")!);
const field = input.getAttribute("key")! as SlotLogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) {
input.value = this.device.getSlotField(slot, field).toString();
}
this.updateDevice();
});
}
_handleDeviceRemoveButton(_e: Event) {
this.removeDialog.show();
}
_removeDialogRemove() {
this.removeDialog.hide();
window.VM.get().then((vm) => vm.removeDevice(this.deviceID));
}
_handleChangeConnection(e: CustomEvent) {
const select = e.target as SlSelect;
const conn = parseInt(select.getAttribute("key")!);
const val = select.value ? parseInt(select.value as string) : undefined;
window.VM.get().then((vm) => vm.setDeviceConnection(this.deviceID, conn, val)
);
this.updateDevice();
}
_handleChangePin(e: CustomEvent) {
const select = e.target as SlSelect;
const pin = parseInt(select.getAttribute("key")!);
const val = select.value ? parseInt(select.value as string) : undefined;
window.VM.get().then((vm) => vm.setDevicePin(this.deviceID, pin, val));
this.updateDevice();
}
}

View File

@@ -1,17 +1,16 @@
import { html, css, HTMLTemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { customElement, property, query} from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceMixin } from "virtual_machine/base_device";
import type { DeviceDB } from "virtual_machine/device_db";
import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils";
import { LogicType, Slot, SlotLogicType, SlotOccupant, SlotType } from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
import "./slot"
@customElement("vm-device-card")
export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
image_err: boolean;
@property({ type: Boolean }) open: boolean;
@@ -117,31 +116,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
`,
];
private _deviceDB: DeviceDB;
get deviceDB(): DeviceDB {
return this._deviceDB;
}
@state()
set deviceDB(val: DeviceDB) {
this._deviceDB = val;
this.updateDevice();
this.requestUpdate();
}
connectedCallback(): void {
super.connectedCallback();
window.VM.vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
);
}
_handleDeviceDBLoad(e: CustomEvent) {
this.deviceDB = e.detail;
}
onImageErr(e: Event) {
this.image_err = true;
console.log("Image load error", e);
@@ -178,7 +152,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}Name.value"></sl-copy-button>
</sl-input>
<sl-input id="vmDeviceCard${this.deviceID}NameHash" size="small" pill class="device-name-hash"
value="${this.nameHash}" disabled>
value="${this.nameHash}" readonly>
<span slot="prefix">Hash</span>
<sl-copy-button slot="suffix" from="vmDeviceCard${this.deviceID}NameHash.value"></sl-copy-button>
</sl-input>
@@ -208,18 +182,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
`;
}
lookupSlotOccupantImg(
occupant: SlotOccupant | undefined,
typ: SlotType,
): string {
if (typeof occupant !== "undefined") {
const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
const prefabName = hashLookup[occupant.prefab_hash] ?? "UnknownHash";
return `img/stationpedia/${prefabName}.png`;
} else {
return `img/stationpedia/SlotIcon_${typ}.png`;
}
}
_onSlotImageErr(e: Event) {
console.log("image_err", e);
@@ -228,54 +190,16 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
static transparentImg =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
renderSlot(slot: Slot, slotIndex: number): HTMLTemplateResult {
const _fields = this.device.getSlotFields(slotIndex);
const fields = Array.from(_fields.entries());
const inputIdBase = `vmDeviceCard${this.deviceID}Slot${slotIndex}Field`;
const slotImg = this.lookupSlotOccupantImg(slot.occupant, slot.typ);
return html`
<sl-card class="slot-card">
<img slot="header" class="slot-header image" src="${slotImg}" onerror="this.src = '${VMDeviceCard.transparentImg}'" />
<span slot="header" class="slot-header">${slotIndex} : ${slot.typ}</span>
${
typeof slot.occupant !== "undefined"
? html`
<span slot="header" class="slot-header">
Occupant: ${slot.occupant.id} : ${slot.occupant.prefab_hash}
</span>
<span slot="header" class="slot-header">
Quantity: ${slot.occupant.quantity}/
${slot.occupant.max_quantity}
</span>
`
: ""
}
<div class="slot-fields">
${fields.map(
([name, field], _index, _fields) => html`
<sl-input
id="${inputIdBase}${name}"
slotIndex=${slotIndex}
key="${name}"
value="${displayNumber(field.value)}"
size="small"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`,
)}
</div>
</sl-card>
`;
}
renderSlots(): HTMLTemplateResult {
return html`
<div class="slots">
${this.slots.map((slot, index, _slots) => this.renderSlot(slot, index))}
${this.slots.map((_slot, index, _slots) => html`
<vm-device-slot
.deviceID=${this.deviceID}
.slotIndex=${index}
>
</vm-device-slot>
` )}
</div>
`;
}
@@ -295,7 +219,7 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
<span slot="prefix">Connection:${index} </span>
${vmNetworks.map(
(net) =>
html`<sl-option value=${net}>Network ${net}</sl-option>`,
html`<sl-option value=${net.toString()}>Network ${net}</sl-option>`,
)}
<span slot="prefix"> ${conn?.typ} </span>
</sl-select>
@@ -415,20 +339,6 @@ export class VMDeviceCard extends VMDeviceMixin(BaseElement) {
this.updateDevice();
});
}
_handleChangeSlotField(e: CustomEvent) {
const input = e.target as SlInput;
const slot = parseInt(input.getAttribute("slotIndex")!);
const field = input.getAttribute("key")! as SlotLogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceSlotField(this.deviceID, slot, field, val, true)) {
input.value = this.device.getSlotField(slot, field).toString();
}
this.updateDevice();
});
}
_handleDeviceRemoveButton(_e: Event) {
this.removeDialog.show();
}

View File

@@ -0,0 +1,181 @@
import { html, css, HTMLTemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
import type { DeviceDB } from "virtual_machine/device_db";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils";
import {
LogicType,
Slot,
SlotLogicType,
SlotOccupant,
SlotType,
} from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
import { VMDeviceCard } from "./card";
import { when } from "lit/directives/when.js";
@customElement("vm-device-slot")
export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
@property({ type: Number }) slotIndex: number;
constructor() {
super();
}
static styles = [
...defaultCss,
css`
.slot-card {
--padding: var(--sl-spacing-small);
}
.slot-card::part(base) {
background-color: var(--sl-color-neutral-50);
}
`,
];
slotOccupantImg(): string {
const slot = this.slots[this.slotIndex];
if (typeof slot.occupant !== "undefined") {
const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash";
return `img/stationpedia/${prefabName}.png`;
} else {
return `img/stationpedia/SlotIcon_${slot.typ}.png`;
}
}
slotOccupantPrefabName(): string {
const slot = this.slots[this.slotIndex];
if (typeof slot.occupant !== "undefined") {
const hashLookup = (this.deviceDB ?? {}).names_by_hash ?? {};
const prefabName = hashLookup[slot.occupant.prefab_hash] ?? "UnknownHash";
return prefabName;
} else {
return undefined;
}
}
renderHeader() {
const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Head`;
const slot = this.slots[this.slotIndex];
const slotImg = this.slotOccupantImg();
const img = html`<img class="w-10 h-10" src="${slotImg}" oanerror="this.src = '${VMDeviceCard.transparentImg}'" />`;
return html`
<div class="flex flex-row w-full me-2">
<div
class="relative border border-neutral-200/40 rounded-lg p-1
hover:ring-2 hover:ring-purple-500 hover:ring-offset-1
hover:ring-offset-purple-500 cursor-pointer me-2"
@click=${this._handleSlotClick}
>
<sl-tooltip content="${this.slotOccupantPrefabName() ?? slot.typ}">
${img}
</sl-tooltip>
${when(
typeof slot.occupant !== "undefined",
() => html`<div
class="absolute bottom-0 right-0 mr-1 mb-1 text-xs
text-neutral-200/90 font-mono bg-neutral-500/40 rounded"
>
<small>${slot.occupant.quantity} / ${slot.occupant.max_quantity}</small>
</div>`
)}
<div></div>
</div>
<div class="ms-4 mt-auto mb-auto">
${when(
typeof slot.occupant !== "undefined",
() => html`
<span class="">
${slot.occupant.id} : ${this.slotOccupantPrefabName()}
</span>
`,
() => html`
<span class="">
${slot.typ}
</span>
`,
)}
</div>
<div class="ms-auto mt-auto mb-auto me-4">
${when(
typeof slot.occupant !== "undefined",
() => html`
<sl-input
type="number"
size="small"
.value=${slot.occupant.quantity.toString()}
.min=${1}
.max=${slot.occupant.max_quantity}
>
<div slot="help-text">
<span><strong>Max Quantity:</strong>${slot.occupant.max_quantity}</span>
</div>
</sl-input>
`,
() => html`
`,
)}
</div>
</div>
`;
}
_handleSlotClick(e: Event) {
console.log(e);
}
renderFields() {
const inputIdBase = `vmDeviceSlot${this.deviceID}Slot${this.slotIndex}Field`;
const _fields = this.device.getSlotFields(this.slotIndex);
const fields = Array.from(_fields.entries());
return html`
<div class="slot-fields">
${fields.map(
([name, field], _index, _fields) => html`
<sl-input
id="${inputIdBase}${name}"
key="${name}"
value="${displayNumber(field.value)}"
size="small"
@sl-change=${this._handleChangeSlotField}
>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>
`,
)}
</div>
`;
}
_handleChangeSlotField(e: CustomEvent) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as SlotLogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceSlotField(this.deviceID, this.slotIndex, field, val, true)) {
input.value = this.device.getSlotField(this.slotIndex, field).toString();
}
this.updateDevice();
});
}
render() {
return html`
<ic10-details class="slot-card">
<div class="slot-header w-full" slot="summary">${this.renderHeader()}</div>
<div class="slot-body">
${this.renderFields()}
</div>
</ic10-details>
`;
}
}

View File

@@ -24,11 +24,10 @@ import { displayNumber, parseNumber } from "utils";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { VMDeviceCard } from "./card";
import { VMDeviceDBMixin } from "virtual_machine/base_device";
@customElement("vm-device-template")
export class VmDeviceTemplate extends BaseElement {
private _deviceDB: DeviceDB;
private image_err: boolean = false;
export class VmDeviceTemplate extends VMDeviceDBMixin(BaseElement) {
static styles = [
...defaultCss,
@@ -70,16 +69,6 @@ export class VmDeviceTemplate extends BaseElement {
this.deviceDB = window.VM.vm.db;
}
get deviceDB(): DeviceDB {
return this._deviceDB;
}
@state()
set deviceDB(val: DeviceDB) {
this._deviceDB = val;
this.setupState();
}
private _prefab_name: string;
get prefab_name(): string {
@@ -138,21 +127,6 @@ export class VmDeviceTemplate extends BaseElement {
this.connections = connections.map((conn) => conn[1]);
}
connectedCallback(): void {
super.connectedCallback();
window.VM.get().then((vm) =>
vm.addEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
),
);
}
_handleDeviceDBLoad(e: CustomEvent) {
this.deviceDB = e.detail;
}
renderFields(): HTMLTemplateResult {
const fields = Object.entries(this.fields);
return html`