Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for asset ability controls. #465

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 88 additions & 5 deletions src/assets/asset-card.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import {
Asset,
AssetAbility,
AssetAbilityControlField,
AssetConditionMeter,
AssetControlField,
AssetOptionField,
DictKey,
} from "@datasworn/core/dist/Datasworn";
import { TemplateResult, html } from "lit-html";
import { TemplateResult, html, nothing } from "lit-html";
import { map } from "lit-html/directives/map.js";
import { range } from "lit-html/directives/range.js";

import { Clock } from "clocks/clock";
import { clockWidget } from "clocks/ui/clock-widget";
import { IDataContext } from "datastore/data-context";
import { produce } from "immer";
import IronVaultPlugin from "index";
import { repeat } from "lit-html/directives/repeat.js";
import { rootLogger } from "logger";
import { md } from "utils/ui/directives";
import { integratedAssetLens } from "../characters/assets";
import { IronVaultSheetAssetSchema } from "../characters/lens";

const logger = rootLogger.getLogger("asset-card");

export function makeDefaultSheetAsset(asset: Asset) {
return {
id: asset._id,
Expand Down Expand Up @@ -194,6 +200,14 @@ function renderAssetAbility(
}),
);
};
const updateControlField = (key: string) =>
updateAsset &&
((control: AssetAbilityControlField) =>
updateAsset(
produce(asset, (draft) => {
draft.abilities[i].controls![key] = control;
}),
));
return html`<li>
<label>
<input
Expand All @@ -204,6 +218,24 @@ function renderAssetAbility(
/>
<span>${md(plugin, ability.text)}</span>
</label>
${ability.controls
? html`<ul class="controls">
${repeat(
Object.entries(ability.controls),
([key]) => key,
([key, control]) => {
return html`<li>
<dl>
<dt>${key}</dt>
<dd class="control">
${renderControl(key, control, updateControlField(key))}
</dd>
</dl>
</li>`;
},
)}
</ul>`
: null}
</li>`;
}

Expand Down Expand Up @@ -242,10 +274,10 @@ function renderControls<T extends Asset | AssetConditionMeter>(
`;
}

function renderControl(
function renderControl<C extends AssetControlField | AssetAbilityControlField>(
key: string,
control: AssetControlField,
updateControl?: (asset: AssetControlField) => void,
control: C,
updateControl?: (control: C) => void,
) {
const updateControlValue = (e: Event) => {
const value = (e.target as HTMLInputElement).value;
Expand Down Expand Up @@ -275,6 +307,14 @@ function renderControl(
}),
);
};
const updateClockValue = (newProgress: number) => {
if (!updateControl) return;
updateControl(
produce(control, (draft) => {
draft.value = newProgress;
}),
);
};

switch (control.field_type) {
case "condition_meter": {
Expand All @@ -283,7 +323,12 @@ function renderControl(
@submit=${(ev: Event) => ev.preventDefault()}
>
${control.controls &&
renderControls(control, control.controls, updateControl)}
renderControls(
control,
control.controls,
// At this point we know this must be an updater for a condition meter
updateControl as (control: AssetConditionMeter) => void,
)}
<ul class="meter">
<li><span>${control.label}</span></li>
${repeat(
Expand Down Expand Up @@ -359,5 +404,43 @@ function renderControl(
</select>
</label>`;
}

case "clock": {
return clockWidget(
Clock.create({
active: true,
name: control.label,
progress: control.value,
segments: control.max,
}).unwrap(),
updateClockValue,
);
}
case "counter": {
return html`<input
type="number"
min="${control.min}"
max="${control.max ?? nothing}"
.value=${control.value}
?disabled=${!updateControl}
@change=${updateControlValueNumeric}
/>`;
}

case "text": {
return html`<input
type="text"
?disabled=${!updateControl}
placeholder=${control.label}
.value=${control.value}
@change=${updateControlValue}
/>`;
}
default:
logger.warn(
"Unsupported asset control: %s",
(control as { field_type: string }).field_type,
);
return html``;
}
}
5 changes: 5 additions & 0 deletions src/assets/css/asset-card.css
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,9 @@
}
}
}

& input[type=number]::-webkit-inner-spin-button {
appearance: auto;
-webkit-appearance: auto;
}
}
45 changes: 7 additions & 38 deletions src/clocks/clock-block.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { html, render, svg } from "lit-html";
import { map } from "lit-html/directives/map.js";
import { range } from "lit-html/directives/range.js";
import { html, render } from "lit-html";

import IronVaultPlugin from "index";
import { vaultProcess } from "utils/obsidian";
Expand All @@ -9,6 +7,7 @@ import { TrackedEntityRenderer } from "utils/ui/tracked-entity-renderer";
import { ZodError } from "zod";
import { Clock } from "./clock";
import { ClockFileAdapter, clockUpdater } from "./clock-file";
import { clockWidget } from "./ui/clock-widget";

export default function registerClockBlock(plugin: IronVaultPlugin): void {
plugin.registerMarkdownCodeBlockProcessor(
Expand Down Expand Up @@ -74,19 +73,11 @@ class ClockRenderer extends TrackedEntityRenderer<ClockFileAdapter, ZodError> {
>`}
</header>

<svg
class="clock-widget"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="-55 -55 110 110"
aria-valuenow=${clockFile.clock.progress}
aria-valuetext="${clockFile.clock.progress}⁄${clockFile.clock
.segments}"
>
${map(range(clockFile.clock.segments), (i) =>
this.renderPath(i, clockFile),
)}
</svg>
${clockWidget(clockFile.clock, (newProgress) =>
this.updateClockProgress({
progress: newProgress,
}),
)}

<div
class="clock-segments"
Expand Down Expand Up @@ -149,15 +140,6 @@ class ClockRenderer extends TrackedEntityRenderer<ClockFileAdapter, ZodError> {
render(tpl, this.containerEl);
}

renderPath(i: number, clockFile: ClockFileAdapter) {
return svg`<path
d="${pathString(i, clockFile.clock.segments)}"
class="clock-segment svg"
aria-selected="${clockFile.clock.progress === i + 1}"
@click=${() => this.updateClockProgress({ progress: i === 0 && clockFile.clock.progress === 1 ? 0 : i + 1 })}
></path>`;
}

async updateClockProgress({
steps,
progress,
Expand All @@ -174,16 +156,3 @@ class ClockRenderer extends TrackedEntityRenderer<ClockFileAdapter, ZodError> {
);
}
}

const R = 50;

function pathString(wedgeIdx: number, numWedges: number) {
const wedgeAngle = (2 * Math.PI) / numWedges;
const startAngle = wedgeIdx * wedgeAngle - Math.PI / 2;
const x1 = R * Math.cos(startAngle);
const y1 = R * Math.sin(startAngle);
const x2 = R * Math.cos(startAngle + wedgeAngle);
const y2 = R * Math.sin(startAngle + wedgeAngle);

return `M0,0 L${x1},${y1} A${R},${R} 0 0,1 ${x2},${y2} z`;
}
107 changes: 54 additions & 53 deletions src/clocks/css/clocks.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
.block-language-iron-vault-clock {
container: iron-vault-clock / inline-size;
}
.iron-vault-clock-widget {
& .clock-segment {
vector-effect: non-scaling-stroke;
}
max-width: 200px;
@container iron-vault-clock (min-width: 300px) {
max-width: 300px;
}
pointer-events: none;
fill: var(--interactive-accent);
fill-opacity: 0.8;
stroke: var(--background-modifier-border);
stroke-width: 4;
aspect-ratio: 1;

&:hover {
fill-opacity: 0.8;

& .clock-segment {
&:hover {
fill: var(--interactive-accent-hover);
& ~ .clock-segment {
fill: var(--interactive-normal);
}
}
}
}

&:not(:hover) {
& .clock-segment {
&[aria-selected="true"] {
& ~ .clock-segment {
fill: var(--interactive-normal);
}
}
}
}

&[aria-valuenow="0"]:not(:hover) {
& .clock-segment {
fill: var(--interactive-normal);
}
}

& .clock-segment {
transition: fill 0.3s ease;
cursor: pointer;
pointer-events: visible;

&:active {
fill-opacity: 1;
}
}
}
.iron-vault-clock {
margin: 0.5em auto;
& > .clock-name {
Expand Down Expand Up @@ -29,58 +83,5 @@
gap: 0.4em;
}
}
& .clock-widget {
& .clock-segment {
vector-effect: non-scaling-stroke;
}
max-width: 200px;
@container iron-vault-clock (min-width: 300px) {
max-width: 300px;
}
pointer-events: none;
fill: var(--interactive-accent);
fill-opacity: 0.8;
stroke: var(--background-modifier-border);
stroke-width: 4;
aspect-ratio: 1;

&:hover {
fill-opacity: 0.8;

& .clock-segment {
&:hover {
fill: var(--interactive-accent-hover);
& ~ .clock-segment {
fill: var(--interactive-normal);
}
}
}
}

&:not(:hover) {
& .clock-segment {
&[aria-selected="true"] {
& ~ .clock-segment {
fill: var(--interactive-normal);
}
}
}
}

&[aria-valuenow="0"]:not(:hover) {
& .clock-segment {
fill: var(--interactive-normal);
}
}

& .clock-segment {
transition: fill 0.3s ease;
cursor: pointer;
pointer-events: visible;

&:active {
fill-opacity: 1;
}
}
}
}
Loading
Loading