Skip to content

Commit d8d89b0

Browse files
committed
abilityの使用回数を管理する機能を追加し、下部にabilityの表示を追加
1 parent 7b25113 commit d8d89b0

File tree

6 files changed

+160
-34
lines changed

6 files changed

+160
-34
lines changed

src/ability.ts

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ export type AbilityInit = {
1010
inventoryIsInfinite?: boolean;
1111
};
1212
export type AbilityEnableOptions = {
13-
copy: boolean;
14-
paste: boolean;
15-
cut: boolean;
13+
// 回数 or Number.POSITIVE_INFINITY
14+
copy: number;
15+
paste: number;
16+
cut: number;
1617
};
1718
type History = {
1819
at: { x: number; y: number };
@@ -22,6 +23,10 @@ type History = {
2223
before: Block | null;
2324
after: Block | null;
2425
};
26+
enabled: {
27+
before: AbilityEnableOptions;
28+
after: AbilityEnableOptions;
29+
};
2530
};
2631
export class AbilityControl {
2732
history: History[] = [];
@@ -32,27 +37,30 @@ export class AbilityControl {
3237
focused: Coords | undefined;
3338
constructor(cx: Context, options?: AbilityInit) {
3439
this.enabled = options?.enabled ?? {
35-
copy: true,
36-
paste: true,
37-
cut: true,
40+
copy: Number.POSITIVE_INFINITY,
41+
paste: Number.POSITIVE_INFINITY,
42+
cut: Number.POSITIVE_INFINITY,
3843
};
3944
this.inventoryIsInfinite = options?.inventoryIsInfinite ?? false;
4045
cx.uiContext.update((prev) => ({
4146
...prev,
4247
inventory: this.inventory,
4348
inventoryIsInfinite: this.inventoryIsInfinite,
49+
...this.enabled,
50+
undo: 0,
51+
redo: 0,
4452
}));
4553
document.addEventListener("copy", (e) => {
4654
e.preventDefault();
47-
if (this.enabled.copy) this.copy(cx);
55+
if (this.enabled.copy > 0) this.copy(cx);
4856
});
4957
document.addEventListener("cut", (e) => {
5058
e.preventDefault();
51-
if (this.enabled.cut) this.cut(cx);
59+
if (this.enabled.cut > 0) this.cut(cx);
5260
});
5361
document.addEventListener("paste", (e) => {
5462
e.preventDefault();
55-
if (this.enabled.paste) this.paste(cx);
63+
if (this.enabled.paste > 0) this.paste(cx);
5664
});
5765
}
5866
setInventory(cx: Context, inventory: Block | null) {
@@ -85,6 +93,10 @@ export class AbilityControl {
8593
const target = cx.grid.getBlock(this.focused.x, this.focused.y);
8694
if (!target || target !== Block.movable) return;
8795
this.setInventory(cx, target);
96+
cx.uiContext.update((prev) => ({
97+
...prev,
98+
copy: --this.enabled.copy,
99+
}));
88100
}
89101
paste(cx: Context) {
90102
if (!this.focused) return;
@@ -96,15 +108,23 @@ export class AbilityControl {
96108
if (!this.inventoryIsInfinite) {
97109
this.setInventory(cx, null);
98110
}
99-
100-
this.pushHistory({
111+
const prevEnabled = { ...this.enabled };
112+
cx.uiContext.update((prev) => ({
113+
...prev,
114+
paste: --this.enabled.paste,
115+
}));
116+
this.pushHistory(cx, {
101117
at: { ...this.focused },
102118
from: Block.air,
103119
to: prevInventory,
104120
inventory: {
105121
before: prevInventory,
106122
after: this.inventory,
107123
},
124+
enabled: {
125+
before: prevEnabled,
126+
after: this.enabled,
127+
},
108128
});
109129
}
110130
cut(cx: Context) {
@@ -115,31 +135,51 @@ export class AbilityControl {
115135
const prevInventory = this.inventory;
116136
this.setInventory(cx, target);
117137
cx.grid.setBlock(cx, this.focused.x, this.focused.y, Block.air);
118-
119-
this.pushHistory({
138+
const prevEnabled = { ...this.enabled };
139+
cx.uiContext.update((prev) => ({
140+
...prev,
141+
cut: --this.enabled.cut,
142+
}));
143+
this.pushHistory(cx, {
120144
at: { ...this.focused },
121145
from: target,
122146
to: Block.air,
123147
inventory: {
124148
before: prevInventory,
125149
after: target,
126150
},
151+
enabled: {
152+
before: prevEnabled,
153+
after: this.enabled,
154+
},
127155
});
128156
}
129157

130158
// History については、 `docs/history-stack.png` を参照のこと
131-
pushHistory(h: History) {
159+
pushHistory(cx: Context, h: History) {
132160
this.history = this.history.slice(0, this.historyIndex);
133161
this.history.push(h);
134162
this.historyIndex = this.history.length;
135163
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
164+
cx.uiContext.update((prev) => ({
165+
...prev,
166+
undo: this.historyIndex,
167+
redo: 0,
168+
}));
136169
}
137170
undo(cx: Context) {
138171
if (this.historyIndex <= 0) return;
139172
this.historyIndex--; // undo は、巻き戻し後の index で計算する
140173
const op = this.history[this.historyIndex];
141174
cx.grid.setBlock(cx, op.at.x, op.at.y, op.from);
142175
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+
}));
143183
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
144184
}
145185
redo(cx: Context) {
@@ -148,20 +188,27 @@ export class AbilityControl {
148188
this.historyIndex++; // redo は、巻き戻し前の index
149189
this.setInventory(cx, op.inventory.after);
150190
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+
}));
151198
console.log(`history: ${this.historyIndex} / ${this.history.length}`);
152199
}
153-
handleKeyDown(cx: Context, e: KeyboardEvent, onGround: boolean) {
200+
handleKeyDown(cx: Context, e: KeyboardEvent /*, onGround: boolean*/) {
154201
if (!(e.ctrlKey || e.metaKey)) return;
155202

156-
if (this.enabled.paste && onGround && e.key === "v") {
157-
this.paste(cx);
158-
}
159-
if (this.enabled.copy && onGround && e.key === "c") {
160-
this.copy(cx);
161-
}
162-
if (this.enabled.cut && onGround && e.key === "x") {
163-
this.cut(cx);
164-
}
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+
// }
165212
if (e.key === "z") {
166213
this.undo(cx);
167214
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: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,71 @@ import type { UIContext } from "@/context.ts";
55
import { setup } from "@/main.ts";
66
import type { StageDefinition } from "@/stages.ts";
77
import { type Writable, writable } from "svelte/store";
8+
import Ability from "./Ability.svelte";
89
910
type Props = { stageNum: string; stage: StageDefinition };
1011
const { stageNum, stage }: Props = $props();
1112
let container: HTMLElement | null = $state(null);
12-
const uiContext = writable<UIContext>({ inventory: null, inventoryIsInfinite: false, });
13-
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+
});
1422
$effect(() => {
1523
if (container) {
1624
setup(container, stage, uiContext);
1725
}
1826
});
1927
</script>
2028

21-
<div bind:this={container} style="width: 100dvw; height: 100dvh; overflow: hidden;">
22-
<div style="position: fixed; left: 0; top: 0; right: 0; background: oklch(82.8% 0.189 84.429 / 40%); display: flex; align-items: baseline; padding: 8px 8px; backdrop-filter: blur(2px);">
23-
<span style="font-size: 32px; margin-right: 6px;">Stage:</span>
24-
<span style="font-size: 40px">{stageNum}</span>
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>
2533
<span style="flex-grow: 1"></span>
26-
<span style="font-size: 24px;">Clipboard:</span>
27-
<div style="width: 48px; height: 48px; margin: 0 6px; align-self: center; border-style: solid; border-width: 4px; border-radius: 6px; border-color: oklch(87.9% 0.169 91.605);">
34+
<span style="font-size: 1.5rem;">Clipboard:</span>
35+
<div class="inventory">
2836
{#if $uiContext.inventory === Block.movable}
2937
<!-- todo: tint 0xff0000 をする必要があるが、そもそもこの画像は仮なのか本当に赤色にするのか -->
3038
<img src="/assets/block.png" width="100%" height="100%"/>
3139
{/if}
3240
</div>
33-
<span style="font-size: 24px; margin-right: 4px;">✕</span>
34-
<span style="font-size: 24px;">{$uiContext.inventoryIsInfinite ? "" : "1"}</span>
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} />
3551
</div>
3652
</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/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.5rem;
17+
border-style: solid;
18+
border-width: 0.2rem;
19+
border-radius: 0.3rem;
20+
}
21+
</style>

src/context.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ export type Context = {
2222
export type UIContext = {
2323
inventory: Block | null;
2424
inventoryIsInfinite: boolean;
25+
copy: number;
26+
paste: number;
27+
cut: number;
28+
undo: number;
29+
redo: number;
2530
};

src/player.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ export class Player {
8383
return highlight;
8484
}
8585
handleInput(_cx: Context, event: KeyboardEvent, eventIsKeyDown: boolean) {
86+
if (eventIsKeyDown) {
87+
this.ability.handleKeyDown(_cx, event /*, this.onGround*/);
88+
}
8689
switch (event.key) {
8790
case "Control":
8891
this.holdingKeys[Inputs.Ctrl] = eventIsKeyDown;

0 commit comments

Comments
 (0)