Skip to content

Commit cc73d84

Browse files
authored
uiの実装など (#2)
* gridをウィンドウの中央揃えにし、リサイズ時の処理も追加 * 上部にステージとインベントリの表示を追加 * abilityの使用回数を管理する機能を追加し、下部にabilityの表示を追加
1 parent 04b11b2 commit cc73d84

File tree

10 files changed

+326
-59
lines changed

10 files changed

+326
-59
lines changed

routes/game/+page.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { page } from "$app/state";
44
import GameLoader from "@/components/GameLoader.svelte";
55
import { stages } from "@/stages";
66
7-
const stageDefinition = $derived(
8-
browser ? stages.get(page.url.searchParams.get("stage") ?? "") : undefined,
7+
const stageNum = $derived(
8+
browser ? (page.url.searchParams.get("stage") ?? "") : "",
99
);
10+
const stageDefinition = stages.get(stageNum);
1011
</script>
1112

12-
<GameLoader stage={stageDefinition}>
13+
<GameLoader stage={stageDefinition} {stageNum}>
1314
{#snippet children(loadingState)}
1415
{loadingState}...
1516
{/snippet}

src/ability.ts

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ export type Coords = {
77
};
88
export type AbilityInit = {
99
enabled?: AbilityEnableOptions;
10+
inventoryIsInfinite?: boolean;
1011
};
1112
export type AbilityEnableOptions = {
12-
copy: boolean;
13-
paste: boolean;
14-
cut: boolean;
13+
// 回数 or Number.POSITIVE_INFINITY
14+
copy: number;
15+
paste: number;
16+
cut: number;
1517
};
1618
type History = {
1719
at: { x: number; y: number };
@@ -21,33 +23,53 @@ type History = {
2123
before: Block | null;
2224
after: Block | null;
2325
};
26+
enabled: {
27+
before: AbilityEnableOptions;
28+
after: AbilityEnableOptions;
29+
};
2430
};
2531
export class AbilityControl {
2632
history: History[] = [];
2733
historyIndex = 0;
2834
inventory: Block | null = null;
29-
inventoryIsInfinite = false;
35+
inventoryIsInfinite: boolean;
3036
enabled: AbilityEnableOptions;
3137
focused: Coords | undefined;
3238
constructor(cx: Context, options?: AbilityInit) {
3339
this.enabled = options?.enabled ?? {
34-
copy: true,
35-
paste: true,
36-
cut: true,
40+
copy: Number.POSITIVE_INFINITY,
41+
paste: Number.POSITIVE_INFINITY,
42+
cut: Number.POSITIVE_INFINITY,
3743
};
44+
this.inventoryIsInfinite = options?.inventoryIsInfinite ?? false;
45+
cx.uiContext.update((prev) => ({
46+
...prev,
47+
inventory: this.inventory,
48+
inventoryIsInfinite: this.inventoryIsInfinite,
49+
...this.enabled,
50+
undo: 0,
51+
redo: 0,
52+
}));
3853
document.addEventListener("copy", (e) => {
3954
e.preventDefault();
40-
if (this.enabled.copy) this.copy(cx);
55+
if (this.enabled.copy > 0) this.copy(cx);
4156
});
4257
document.addEventListener("cut", (e) => {
4358
e.preventDefault();
44-
if (this.enabled.cut) this.cut(cx);
59+
if (this.enabled.cut > 0) this.cut(cx);
4560
});
4661
document.addEventListener("paste", (e) => {
4762
e.preventDefault();
48-
if (this.enabled.paste) this.paste(cx);
63+
if (this.enabled.paste > 0) this.paste(cx);
4964
});
5065
}
66+
setInventory(cx: Context, inventory: Block | null) {
67+
this.inventory = inventory;
68+
cx.uiContext.update((prev) => ({
69+
...prev,
70+
inventory,
71+
}));
72+
}
5173
highlightCoord(playerAt: Coords, facing: Facing) {
5274
let dx: number;
5375
switch (facing) {
@@ -70,7 +92,11 @@ export class AbilityControl {
7092
if (!this.focused) return;
7193
const target = cx.grid.getBlock(this.focused.x, this.focused.y);
7294
if (!target || target !== Block.movable) return;
73-
this.inventory = target;
95+
this.setInventory(cx, target);
96+
cx.uiContext.update((prev) => ({
97+
...prev,
98+
copy: --this.enabled.copy,
99+
}));
74100
}
75101
paste(cx: Context) {
76102
if (!this.focused) return;
@@ -80,17 +106,25 @@ export class AbilityControl {
80106
const prevInventory = this.inventory;
81107
cx.grid.setBlock(cx, this.focused.x, this.focused.y, this.inventory);
82108
if (!this.inventoryIsInfinite) {
83-
this.inventory = null;
109+
this.setInventory(cx, null);
84110
}
85-
86-
this.pushHistory({
111+
const prevEnabled = { ...this.enabled };
112+
cx.uiContext.update((prev) => ({
113+
...prev,
114+
paste: --this.enabled.paste,
115+
}));
116+
this.pushHistory(cx, {
87117
at: { ...this.focused },
88118
from: Block.air,
89119
to: prevInventory,
90120
inventory: {
91121
before: prevInventory,
92122
after: this.inventory,
93123
},
124+
enabled: {
125+
before: prevEnabled,
126+
after: this.enabled,
127+
},
94128
});
95129
}
96130
cut(cx: Context) {
@@ -99,55 +133,82 @@ export class AbilityControl {
99133
// removable 以外はカットできない
100134
if (!target || target !== Block.movable) return;
101135
const prevInventory = this.inventory;
102-
this.inventory = target;
136+
this.setInventory(cx, target);
103137
cx.grid.setBlock(cx, this.focused.x, this.focused.y, Block.air);
104-
105-
this.pushHistory({
138+
const prevEnabled = { ...this.enabled };
139+
cx.uiContext.update((prev) => ({
140+
...prev,
141+
cut: --this.enabled.cut,
142+
}));
143+
this.pushHistory(cx, {
106144
at: { ...this.focused },
107145
from: target,
108146
to: Block.air,
109147
inventory: {
110148
before: prevInventory,
111149
after: target,
112150
},
151+
enabled: {
152+
before: prevEnabled,
153+
after: this.enabled,
154+
},
113155
});
114156
}
115157

116158
// History については、 `docs/history-stack.png` を参照のこと
117-
pushHistory(h: History) {
159+
pushHistory(cx: Context, h: History) {
118160
this.history = this.history.slice(0, this.historyIndex);
119161
this.history.push(h);
120162
this.historyIndex = this.history.length;
121163
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
164+
cx.uiContext.update((prev) => ({
165+
...prev,
166+
undo: this.historyIndex,
167+
redo: 0,
168+
}));
122169
}
123170
undo(cx: Context) {
124171
if (this.historyIndex <= 0) return;
125172
this.historyIndex--; // undo は、巻き戻し後の index で計算する
126173
const op = this.history[this.historyIndex];
127174
cx.grid.setBlock(cx, op.at.x, op.at.y, op.from);
128-
this.inventory = op.inventory.before;
175+
this.setInventory(cx, op.inventory.before);
176+
this.enabled = op.enabled.before;
177+
cx.uiContext.update((prev) => ({
178+
...prev,
179+
...this.enabled,
180+
undo: this.historyIndex,
181+
redo: this.history.length - this.historyIndex,
182+
}));
129183
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
130184
}
131185
redo(cx: Context) {
132186
if (this.historyIndex >= this.history.length) return;
133187
const op = this.history[this.historyIndex];
134188
this.historyIndex++; // redo は、巻き戻し前の index
135-
this.inventory = op.inventory.after;
189+
this.setInventory(cx, op.inventory.after);
136190
cx.grid.setBlock(cx, op.at.x, op.at.y, op.to);
191+
this.enabled = op.enabled.after;
192+
cx.uiContext.update((prev) => ({
193+
...prev,
194+
...this.enabled,
195+
undo: this.historyIndex,
196+
redo: this.history.length - this.historyIndex,
197+
}));
137198
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
138199
}
139-
handleKeyDown(cx: Context, e: KeyboardEvent, onGround: boolean) {
200+
handleKeyDown(cx: Context, e: KeyboardEvent /*, onGround: boolean*/) {
140201
if (!(e.ctrlKey || e.metaKey)) return;
141202

142-
if (this.enabled.paste && onGround && e.key === "v") {
143-
this.paste(cx);
144-
}
145-
if (this.enabled.copy && onGround && e.key === "c") {
146-
this.copy(cx);
147-
}
148-
if (this.enabled.cut && onGround && e.key === "x") {
149-
this.cut(cx);
150-
}
203+
// if (this.enabled.paste > 0 && onGround && e.key === "v") {
204+
// this.paste(cx);
205+
// }
206+
// if (this.enabled.copy > 0 && onGround && e.key === "c") {
207+
// this.copy(cx);
208+
// }
209+
// if (this.enabled.cut > 0 && onGround && e.key === "x") {
210+
// this.cut(cx);
211+
// }
151212
if (e.key === "z") {
152213
this.undo(cx);
153214
e.preventDefault();

src/components/Ability.svelte

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
import Key from "./Key.svelte";
3+
type Props = { name: string; key: string; num: number };
4+
const { name, num, key }: Props = $props();
5+
</script>
6+
<span style="color: {num > 0 ? "black" : "gray"}">
7+
<Key {key} enabled={num > 0} />
8+
<span style="font-size: 1.5rem;">{name}</span>
9+
<span style="font-size: 1.5rem;">✕</span>
10+
<span style="font-size: 2rem; margin-right: 1rem;">{isFinite(num) ? num : ""}</span>
11+
</span>

src/components/Game.svelte

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,75 @@
11
<script lang="ts">
2+
import { Block } from "@/constants.ts";
3+
import type { UIContext } from "@/context.ts";
24
// client-only.
35
import { setup } from "@/main.ts";
46
import type { StageDefinition } from "@/stages.ts";
7+
import { type Writable, writable } from "svelte/store";
8+
import Ability from "./Ability.svelte";
59
6-
type Props = { stage: StageDefinition };
7-
const { stage }: Props = $props();
10+
type Props = { stageNum: string; stage: StageDefinition };
11+
const { stageNum, stage }: Props = $props();
812
let container: HTMLElement | null = $state(null);
9-
13+
const uiContext = writable<UIContext>({
14+
inventory: null,
15+
inventoryIsInfinite: false,
16+
copy: 0,
17+
cut: 0,
18+
paste: 0,
19+
undo: 0,
20+
redo: 0,
21+
});
1022
$effect(() => {
1123
if (container) {
12-
setup(container, stage);
24+
setup(container, stage, uiContext);
1325
}
1426
});
1527
</script>
1628

17-
<div id="app">
18-
<div bind:this={container}></div>
29+
<div bind:this={container} class="container">
30+
<div class="uiBackground" style="position: fixed; left: 0; top: 0; right: 0; display: flex; align-items: baseline;">
31+
<span style="font-size: 2rem; margin-right:0.5rem;">Stage:</span>
32+
<span style="font-size: 2.5rem">{stageNum}</span>
33+
<span style="flex-grow: 1"></span>
34+
<span style="font-size: 1.5rem;">Clipboard:</span>
35+
<div class="inventory">
36+
{#if $uiContext.inventory === Block.movable}
37+
<!-- todo: tint 0xff0000 をする必要があるが、そもそもこの画像は仮なのか本当に赤色にするのか -->
38+
<img src="/assets/block.png" width="100%" height="100%"/>
39+
{/if}
40+
</div>
41+
<span style="font-size: 1.5rem;">✕</span>
42+
<span style="font-size: 2rem;">{$uiContext.inventoryIsInfinite ? "" : "1"}</span>
43+
</div>
44+
<div class="uiBackground" style="position: fixed; left: 0; bottom: 0; right: 0; display: flex; align-items: baseline;">
45+
<span style="font-size: 1.5rem; margin-right: 1rem;">Abilities:</span>
46+
<Ability key="C" name="Copy" num={$uiContext.copy} />
47+
<Ability key="X" name="Cut" num={$uiContext.cut} />
48+
<Ability key="V" name="Paste" num={$uiContext.paste} />
49+
<Ability key="Z" name="Undo" num={$uiContext.undo} />
50+
<Ability key="Y" name="Redo" num={$uiContext.redo} />
51+
</div>
1952
</div>
53+
54+
<style>
55+
.container {
56+
width: 100dvw;
57+
height: 100dvh;
58+
overflow: hidden;
59+
}
60+
.uiBackground {
61+
background: oklch(82.8% 0.189 84.429 / 40%);
62+
backdrop-filter: blur(2px);
63+
padding: 0.75rem 1rem;
64+
}
65+
.inventory {
66+
width: 3rem;
67+
height: 3rem;
68+
margin: 0 0.5rem;
69+
align-self: center;
70+
border-style: solid;
71+
border-width: 0.3rem;
72+
border-radius: 0.5rem;
73+
border-color: oklch(87.9% 0.169 91.605);
74+
}
75+
</style>

src/components/GameLoader.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ import type { Snippet } from "svelte";
66
77
type Props = {
88
children: Snippet<[string]>;
9+
stageNum: string;
910
stage: StageDefinition | undefined;
1011
};
11-
const { children, stage }: Props = $props();
12+
const { children, stageNum, stage }: Props = $props();
1213
</script>
1314

1415
{#if browser}
1516
{#await import("@/components/Game.svelte")}
1617
{@render children("Downloading")}
1718
{:then { default: Game }}
1819
{#if stage}
19-
<Game {stage} />
20+
<Game {stageNum} {stage} />
2021
{:else}
2122
Stage not found! <a href="/">Go Back</a>
2223
{/if}

src/components/Key.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
type Props = { key: string; enabled: boolean };
3+
const { key, enabled }: Props = $props();
4+
let isMacOS: boolean = $state(false);
5+
$effect(() => {
6+
isMacOS = navigator.userAgent.includes("Mac OS X");
7+
});
8+
</script>
9+
<span class="key" style="border-color: {enabled ? "black" : "gray"}; background: {enabled ? "white" : "lightgray"};">
10+
{isMacOS ? "⌘+" + key : "Ctrl+" + key}
11+
</span>
12+
<style>
13+
.key{
14+
display: inline-block;
15+
font-size: 1rem;
16+
padding: 0.3rem;
17+
border-style: solid;
18+
border-width: 0.2rem;
19+
border-radius: 0.3rem;
20+
}
21+
</style>

0 commit comments

Comments
 (0)