11<script lang =" ts" >
22import Key from " @/ui-components/Key.svelte" ;
33import { onMount } from " svelte" ;
4+ import " @/ui-components/menu/menu.css" ;
5+ import { replaceState } from " $app/navigation" ;
46
5- let w = " 1" ;
7+ let world: string | null = null ;
8+ let selected: number | null = null ;
69
710onMount (() => {
811 const params = new URLSearchParams (window .location .search );
9- w = params .get (" w" ) ?? " 1" ;
12+ world = params .get (" w" ) ?? " 1" ;
13+ selected = Number (params .get (" s" ) ?? " 1" ) - 1 ;
1014});
1115
1216$ : blocks = [
13- { label: " 1" , link: ` /game?stage=${w }-1 ` },
14- { label: " 2" , link: ` /game?stage=${w }-2 ` },
15- { label: " 3" , link: ` /game?stage=${w }-3 ` },
16- { label: " 4" , link: ` /game?stage=${w }-4 ` },
17+ { label: " 1" , link: ` /game?stage=${world }-1 ` },
18+ { label: " 2" , link: ` /game?stage=${world }-2 ` },
19+ { label: " 3" , link: ` /game?stage=${world }-3 ` },
20+ { label: " 4" , link: ` /game?stage=${world }-4 ` },
1721];
1822
19- let selected = 0 ;
2023let lastKeyTime = 0 ;
24+ let lastKey: string | null = null ;
2125const KEY_REPEAT_DELAY = 180 ; // ms
2226
27+ function prevWorld() {
28+ // wを数値として1減らす(1未満にはしない)
29+ world = String (Math .max (1 , Number (world ) - 1 ));
30+ const url = new URL (window .location .href );
31+ url .searchParams .set (" w" , world );
32+ replaceState (url .toString (), {});
33+ }
34+ function nextWorld() {
35+ // wを数値として1増やす(4より大きくしない)
36+ world = String (Math .min (4 , Number (world ) + 1 ));
37+ const url = new URL (window .location .href );
38+ url .searchParams .set (" w" , world );
39+ replaceState (url .toString (), {});
40+ }
41+ function select(index : number ): void {
42+ selected = index ;
43+ const url = new URL (window .location .href );
44+ url .searchParams .set (" s" , String (index + 1 ));
45+ replaceState (url .toString (), {});
46+ }
47+
2348function handleKey(e : KeyboardEvent ): void {
2449 const now = Date .now ();
2550 if (e .key === " ArrowRight" || e .key === " ArrowLeft" ) {
26- if (now - lastKeyTime < KEY_REPEAT_DELAY ) {
51+ if (lastKey === e . key && now - lastKeyTime < KEY_REPEAT_DELAY ) {
2752 return ;
2853 }
2954 lastKeyTime = now ;
55+ lastKey = e .key ;
56+ }
57+
58+ if (selected === null ) {
59+ return ;
3060 }
3161
3262 if (e .key === " ArrowRight" ) {
33- selected = (selected + 1 ) % blocks .length ;
63+ if (selected === blocks .length - 1 ) {
64+ if (world !== " 4" ) {
65+ nextWorld ();
66+ select (0 );
67+ }
68+ } else {
69+ select (selected + 1 );
70+ }
3471 } else if (e .key === " ArrowLeft" ) {
35- selected = (selected - 1 + blocks .length ) % blocks .length ;
72+ if (selected === 0 ) {
73+ if (world !== " 1" ) {
74+ prevWorld ();
75+ select (blocks .length - 1 );
76+ }
77+ } else {
78+ select (selected - 1 );
79+ }
3680 } else if (e .key === " Enter" || e .key === " " ) {
3781 window .location .href = blocks [selected ].link ;
82+ } else if (e .key === " Escape" ) {
83+ window .location .href = " /" ;
84+ e .preventDefault ();
3885 }
3986}
40-
41- function handleClick( index : number ) : void {
42- selected = index ;
87+ function handleKeyUp() {
88+ lastKeyTime = 0 ;
89+ lastKey = null ;
4390}
4491
4592let container: HTMLDivElement | null = null ;
4693
4794onMount (() => {
48- container ?.focus ();
95+ document .addEventListener (" keydown" , handleKey );
96+ document .addEventListener (" keyup" , handleKeyUp );
97+ return () => {
98+ document .removeEventListener (" keydown" , handleKey );
99+ document .removeEventListener (" keyup" , handleKeyUp );
100+ };
49101});
50-
51- // Todo?: ワールド選択の矢印をクリックすると、矢印キーを押してもステージが移動できない(=ブロックをクリックする必要あり)
52- // TODO?: Enterキーを押すと、カーソルがある場所のブロックの色が変わる(正しいステージには飛ぶから実害はない)
53- // Todo: 画像をステージごとに変える
54- // TODO: ゲームメニューからstage-selectに飛ばす
55102 </script >
56103
57104<div id =" container" class =" fixed inset-0" >
58- <div class =" fixed inset-0 backdrop-blur-xs" >
59-
60- <div class =" p-10 text-8xl text-center flex items-center justify-center gap-8" >
105+ <div class =" w-full h-full py-8 backdrop-blur-xs flex flex-col" >
106+ <div class =" " >
107+ <button class =" btn modal-btn text-xl ml-8 mb-6 w-max! px-4!" >
108+ <a href =" /" >
109+ <
110+ <Key key =" Esc" enabled />
111+ Back to Main Menu
112+ </a >
113+ </button >
114+ </div >
115+ <div class =" text-7xl text-center flex items-center justify-center gap-8" >
61116 <!-- 左矢印ボタン -->
62117 <button
63- class =" px-4 select-none cursor-pointer"
118+ class ="appearance-none focus:outline-none px-4 select-none cursor-pointer {Number (world ) <= 1
119+ ? ' invisible'
120+ : ' ' } hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"
64121 aria-label =" 前のワールド"
65- on:click ={() => {
66- // wを数値として1減らす(1未満にはしない)
67- const next = Math .max (1 , Number (w ) - 1 );
68- const url = new URL (window .location .href );
69- url .searchParams .set (" w" , String (next ));
70- window .location .href = url .toString ();
71- }}
72- disabled ={Number (w ) <= 1 }
122+ on:click ={prevWorld }
123+ disabled ={Number (world ) <= 1 }
73124 >
74125 <
75126 </button >
76- <span >W{ w }</span >
127+ <span >World { world }</span >
77128 <!-- 右矢印ボタン -->
78129 <button
79- class =" px-4 select-none cursor-pointer"
130+ class ="appearance-none focus:outline-none px-4 select-none cursor-pointer {Number (world ) >= 4
131+ ? ' invisible'
132+ : ' ' } hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"
80133 aria-label =" 次のワールド"
81- on:click ={() => {
82- // wを数値として1増やす(4より大きくしない)
83- const next = Math .min (4 , Number (w ) + 1 );
84- const url = new URL (window .location .href );
85- url .searchParams .set (" w" , String (next ));
86- window .location .href = url .toString ();
87- }}
88- disabled ={Number (w ) >= 4 }
134+ on:click ={nextWorld }
135+ disabled ={Number (world ) >= 4 }
89136 >
90137 >
91138 </button >
92139 </div >
93140
94- <div class =" flex justify-center items-center h-64" >
95- <div
96- bind:this ={container }
97- role =" button"
98- tabindex =" 0"
99- on:keydown ={handleKey }
100- class =" flex outline-none items-center"
101- >
141+ <div class =" flex justify-center items-center grow-1" >
142+ <div role =" button" tabindex =" 0" class =" flex outline-none items-center" >
102143 {#each blocks as block , i }
103144 <button
104145 type =" button"
105- class ={` bg-white border-6 pt-8 pb-6 pl-8 pr-6 transition-colors duration-200 text-7xl cursor-pointer ${
106- selected === i ? ' border-red-500 ring ring-red-500' : ' border-base'
146+ class ={` appearance-none focus:outline-none bg-white border-6 pt-8 pb-6 pl-8 pr-6 transition-colors duration-200 text-7xl cursor-pointer ${
147+ selected === i
148+ ? " border-red-500 ring ring-red-500 bg-amber-100!"
149+ : " border-base"
107150 } ` }
108- on:click ={() => handleClick (i )}
151+ on:click ={() => select (i )}
109152 >
110153 {block .label }
111154 </button >
@@ -116,11 +159,27 @@ onMount(() => {
116159 {/each }
117160 </div >
118161 </div >
119- <div class =" relative flex justify-center items-center h-96 mb-10" >
162+ <div
163+ class =" flex justify-center items-center basis-2/5 min-h-0 shrink grow-2"
164+ >
120165 <!-- 画像を中央に配置 -->
121- <img src =" /assets/thumbnaildev.png" alt =" " class =" h-80 absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
166+ <div class =" h-full" >
167+ {#if selected !== null }
168+ {#key world }
169+ {#each { length : 4 } as _ , idx }
170+ <img
171+ src ="/assets/thumbnail {world }- {idx + 1 }.png"
172+ alt =" "
173+ class ={[" h-full skeleton" , idx !== selected && " hidden" ]}
174+ />
175+ {/each }
176+ {/ key }
177+ {/if }
178+ </div >
122179 <!-- テキストを画像の右側に配置 -->
123- <div class =" absolute left-1/2 top-1/2 -translate-y-1/2 ml-80 flex flex-col items-start bg-white/90 p-4 rounded-lg border-2" >
180+ <div
181+ class =" flex-none w-max max-h-full flex flex-col items-start bg-white/90 p-4 m-4 rounded-lg border-2"
182+ >
124183 Press <Key key =" Enter" enabled /> or <Key key =" Space" enabled /> to start
125184 </div >
126185 </div >
@@ -130,10 +189,13 @@ onMount(() => {
130189<style >
131190 #container {
132191 background-color : black ;
133- background-image : url (' /assets/home.png' ), url (' /assets/home-block.png' );
192+ background-image : url (" /assets/home.png" ), url (" /assets/home-block.png" );
134193 background-size : 100% , 100% ;
135194 background-repeat : no-repeat , repeat-y ;
136- background-position : left top , left , top ;
195+ background-position :
196+ left top ,
197+ left ,
198+ top ;
137199 backdrop-filter : blur (10px );
138200 }
139- </style >
201+ </style >
0 commit comments