MORE
Browse files- README.md +121 -1
- package-lock.json +580 -1
- package.json +8 -2
- public/images/default_trainer.png +3 -0
- public/images/environments/cave.png +3 -0
- public/images/environments/dinner.png +3 -0
- public/images/environments/road.png +3 -0
- public/images/environments/ruins.png +3 -0
- src/lib/components/Battle/ActionButtons.svelte +148 -0
- src/lib/components/Battle/BattleControls.svelte +317 -0
- src/lib/components/Battle/BattleField.svelte +196 -0
- src/lib/components/Battle/PicletInfo.svelte +124 -0
- src/lib/components/Pages/Battle.svelte +205 -0
- src/lib/components/Pages/Encounters.svelte +164 -14
- src/lib/components/Piclets/AddToRosterDialog.svelte +1 -1
- src/lib/components/Piclets/PicletDetail.svelte +1 -1
- src/lib/db/battleService.ts +150 -0
- src/lib/db/encounterService.ts +69 -26
- src/lib/db/resetGame.ts +32 -0
- src/lib/db/schema.ts +30 -0
- src/main.ts +1 -0
- src/tests/encounterService.test.ts +189 -0
- src/tests/setup.ts +12 -0
- vitest.config.ts +16 -0
README.md
CHANGED
@@ -12,7 +12,127 @@ hf_oauth_scopes:
|
|
12 |
- inference-api
|
13 |
---
|
14 |
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
# Svelte + TS + Vite
|
18 |
|
|
|
12 |
- inference-api
|
13 |
---
|
14 |
|
15 |
+
# Piclets
|
16 |
+
Monster collection & battle game leveraging Huggingface ZeroGPU spaces!
|
17 |
+
|
18 |
+
[Play Here!](https://fraser-piclets.static.hf.space/)
|
19 |
+
|
20 |
+
## Battle System
|
21 |
+
|
22 |
+
Making everything LLM based!
|
23 |
+
|
24 |
+
The LLM gets the piclet descriptions, stats and action descriptions and then creates a JSON object conveying the status updates (including emojis for effects).
|
25 |
+
|
26 |
+
This should give the battles a unique "real" feel with interesting effective/ineffective interactions.
|
27 |
+
|
28 |
+
Status updates come with probabilities allowing for chance in the game while LLM sampling temperature is low.
|
29 |
+
|
30 |
+
The LLM backed has a dynamic setup so it can switch between providers, for now will use `tencent/Hunyuan-Large` long term may switch to `BlinkDL/RWKV-Gradio-2`.
|
31 |
+
|
32 |
+
Options for LLM backend:
|
33 |
+
1. tencent/Hunyuan-Large (has been free for a while!)
|
34 |
+
2. yuntian-deng/ChatGPT (say it will have short term availability)
|
35 |
+
3. hysts/zephyr-7b (runs on zero-gpu)
|
36 |
+
4. BlinkDL/RWKV-Gradio-2 (running on T4, never ran into rate limits)
|
37 |
+
5. make my own Cohere space (20 generations per minute)
|
38 |
+
|
39 |
+
## Old Battle System Ideas
|
40 |
+
|
41 |
+
## How to handle combat?
|
42 |
+
I want a combat system that leverages the diversity of monsters and creativity of LLMs.
|
43 |
+
I'd rather the game ran "offline" so without the need for non borwser-based computation.
|
44 |
+
|
45 |
+
I'd also like the combat to feel interesting with a diversity of Piclets being required.
|
46 |
+
Now for most players they won't actually be able to catch that many Piclets (1 per day).
|
47 |
+
|
48 |
+
By interesting I mean the unique properties of the monster should shine through despite them not being explicitly coded for.
|
49 |
+
The simplest version of this is having "weak against, robust against" lists and using a semantic encoder matches to define effectiveness.
|
50 |
+
|
51 |
+
I previously liked the idea of monsters having a "status" that would then effect combat, e.g. burning, bleading, sleeping, etc.
|
52 |
+
This ofc also matches Pokemon though it does seem a little tricky to code and connect all of this.
|
53 |
+
|
54 |
+
Should also note that I can have a TON of data within my HF space so coding up a ton of status effect options is fine!
|
55 |
+
|
56 |
+
#### Combat Ideas
|
57 |
+
|
58 |
+
**Semantic top-trumps**
|
59 |
+
|
60 |
+
You attack with a given trait e.g. "flight" they then pick a counter monster to compare on that axis.
|
61 |
+
The monster with the better semantic score wins.
|
62 |
+
|
63 |
+
A variation on this is each attack description vs the monsters description.
|
64 |
+
|
65 |
+
TBH 1v1 battles with just 1 move doesn't feel tactical enough.
|
66 |
+
|
67 |
+
**True Pokemon**
|
68 |
+
|
69 |
+
Just define a full running pokemon style game with the Pokemon suite of moves.
|
70 |
+
Could even extend to include special abilities like being invulnerable to drain, can't be switched out, etc.
|
71 |
+
|
72 |
+
This would mean abilities would be defined ahead of time and picked for the monster.
|
73 |
+
|
74 |
+
Nice thing is that this would balance the same way and have a high complexity ceiling.
|
75 |
+
The bad thing is that this could actually be extremely difficult to implement, likely best done by just emulating the underlying game with modified sprites & names for the monster.
|
76 |
+
|
77 |
+
**LLM powered Pokemon**
|
78 |
+
|
79 |
+
Leverage free LLM spaces (maybe RWKV?) to have the LLM generate the outcomes of the different action.
|
80 |
+
Include updating free-form status descriptions.
|
81 |
+
|
82 |
+
This should give a consistent range of effects without requiring complex code.
|
83 |
+
|
84 |
+
The issue here is with accessing the LLM responses.
|
85 |
+
This would have to rely on non zero-GPU spaces to get the response rate required.
|
86 |
+
|
87 |
+
**Semantic Pokemon**
|
88 |
+
|
89 |
+
Define pokemon style moves with keywords defining strong against, weak against and action special cases.
|
90 |
+
|
91 |
+
This is effectively doing what "LLM powered Pokemon" would do but offline.
|
92 |
+
|
93 |
+
Different effects have different probabilities.
|
94 |
+
Statuses are applied and semanticly matched into hard coded ones.
|
95 |
+
|
96 |
+
e.g. "Dazed" is similar to "Confusion" so gets the "Confusion" effect
|
97 |
+
|
98 |
+
#### Combat Choice
|
99 |
+
|
100 |
+
I think Semantic Pokemon is the best choice as it gives great variety while being runnable offline.
|
101 |
+
|
102 |
+
To put this in more detail...
|
103 |
+
1. monsters each have lists of keywords for "weak to" and "robust to" for effective/ineffective
|
104 |
+
2. each action has hard coded effects with special cases for certain monsters
|
105 |
+
3. monster actions are colored to indicate attack/buff/debuff/special
|
106 |
+
|
107 |
+
When hard coding effects the action can apply descriptors to the enemy, these then interact with game-level effects and the monsters own effective/ineffective.
|
108 |
+
Previously I did like the emoji setup as it was quick to communicate... but it often wasn't high level enough.
|
109 |
+
I'm hopeful that high level descriptors + a semantic encoder will give more matches than previously.
|
110 |
+
|
111 |
+
So the monster schema will have:
|
112 |
+
```json
|
113 |
+
"robust against": {"type": "string", "description": "A brief description of things the monster is robust to"},
|
114 |
+
"weak against": {"type": "string", "description": "A brief description of things the monster is weak to"},
|
115 |
+
```
|
116 |
+
|
117 |
+
Then for the actions:
|
118 |
+
```json
|
119 |
+
"attack": {
|
120 |
+
"name": "...",
|
121 |
+
"description": "...",
|
122 |
+
"damage": "likert",
|
123 |
+
"accuracy": "likert",
|
124 |
+
"inflictStatus": "...",
|
125 |
+
}
|
126 |
+
```
|
127 |
+
|
128 |
+
### Keyword based
|
129 |
+
|
130 |
+
Another option is to just have claude create a really extensive keyword system to analyze action descriptions and make a series of classifiers based on that.
|
131 |
+
I have had success with this before and its nice to think I then only have to handle the action descriptions.
|
132 |
+
Then I could add back the old Piclet types to pair up with this.
|
133 |
+
Plus then include an emoji for each action to give a cool animation effect!
|
134 |
+
|
135 |
+
The bad thing here is that I am having to hand-code a battle system which I didn't really want to do.
|
136 |
|
137 |
# Svelte + TS + Vite
|
138 |
|
package-lock.json
CHANGED
@@ -14,10 +14,14 @@
|
|
14 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
15 |
"@tsconfig/svelte": "^5.0.4",
|
16 |
"@types/node": "^24.0.14",
|
|
|
|
|
|
|
17 |
"svelte": "^5.28.1",
|
18 |
"svelte-check": "^4.1.6",
|
19 |
"typescript": "~5.8.3",
|
20 |
-
"vite": "^6.3.5"
|
|
|
21 |
}
|
22 |
},
|
23 |
"node_modules/@ampproject/remapping": {
|
@@ -515,6 +519,13 @@
|
|
515 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
516 |
}
|
517 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
518 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
519 |
"version": "4.45.1",
|
520 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
|
@@ -852,6 +863,23 @@
|
|
852 |
"dev": true,
|
853 |
"license": "MIT"
|
854 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
855 |
"node_modules/@types/estree": {
|
856 |
"version": "1.0.8",
|
857 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
@@ -869,6 +897,150 @@
|
|
869 |
"undici-types": "~7.8.0"
|
870 |
}
|
871 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
872 |
"node_modules/acorn": {
|
873 |
"version": "8.15.0",
|
874 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
@@ -892,6 +1064,16 @@
|
|
892 |
"node": ">= 0.4"
|
893 |
}
|
894 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
895 |
"node_modules/axobject-query": {
|
896 |
"version": "4.1.0",
|
897 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
@@ -902,6 +1084,43 @@
|
|
902 |
"node": ">= 0.4"
|
903 |
}
|
904 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
905 |
"node_modules/chokidar": {
|
906 |
"version": "4.0.3",
|
907 |
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
@@ -946,6 +1165,16 @@
|
|
946 |
}
|
947 |
}
|
948 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
949 |
"node_modules/deepmerge": {
|
950 |
"version": "4.3.1",
|
951 |
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
@@ -962,6 +1191,13 @@
|
|
962 |
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
963 |
"license": "Apache-2.0"
|
964 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
965 |
"node_modules/esbuild": {
|
966 |
"version": "0.25.6",
|
967 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
@@ -1021,6 +1257,36 @@
|
|
1021 |
"@jridgewell/sourcemap-codec": "^1.4.15"
|
1022 |
}
|
1023 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1024 |
"node_modules/fdir": {
|
1025 |
"version": "6.4.6",
|
1026 |
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
@@ -1036,6 +1302,20 @@
|
|
1036 |
}
|
1037 |
}
|
1038 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1039 |
"node_modules/fsevents": {
|
1040 |
"version": "2.3.3",
|
1041 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
@@ -1051,6 +1331,38 @@
|
|
1051 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
1052 |
}
|
1053 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1054 |
"node_modules/is-reference": {
|
1055 |
"version": "3.0.3",
|
1056 |
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
@@ -1061,6 +1373,13 @@
|
|
1061 |
"@types/estree": "^1.0.6"
|
1062 |
}
|
1063 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1064 |
"node_modules/kleur": {
|
1065 |
"version": "4.1.5",
|
1066 |
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
@@ -1078,6 +1397,13 @@
|
|
1078 |
"dev": true,
|
1079 |
"license": "MIT"
|
1080 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1081 |
"node_modules/magic-string": {
|
1082 |
"version": "0.30.17",
|
1083 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
@@ -1098,6 +1424,16 @@
|
|
1098 |
"node": ">=4"
|
1099 |
}
|
1100 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1101 |
"node_modules/ms": {
|
1102 |
"version": "2.1.3",
|
1103 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
@@ -1124,6 +1460,23 @@
|
|
1124 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
1125 |
}
|
1126 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1127 |
"node_modules/picocolors": {
|
1128 |
"version": "1.1.1",
|
1129 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
@@ -1240,6 +1593,28 @@
|
|
1240 |
"node": ">=6"
|
1241 |
}
|
1242 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1243 |
"node_modules/source-map-js": {
|
1244 |
"version": "1.2.1",
|
1245 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
@@ -1250,6 +1625,33 @@
|
|
1250 |
"node": ">=0.10.0"
|
1251 |
}
|
1252 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1253 |
"node_modules/svelte": {
|
1254 |
"version": "5.36.1",
|
1255 |
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.1.tgz",
|
@@ -1300,6 +1702,20 @@
|
|
1300 |
"typescript": ">=5.0.0"
|
1301 |
}
|
1302 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1303 |
"node_modules/tinyglobby": {
|
1304 |
"version": "0.2.14",
|
1305 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
@@ -1317,6 +1733,46 @@
|
|
1317 |
"url": "https://github.com/sponsors/SuperchupuDev"
|
1318 |
}
|
1319 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1320 |
"node_modules/typescript": {
|
1321 |
"version": "5.8.3",
|
1322 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
@@ -1413,6 +1869,29 @@
|
|
1413 |
}
|
1414 |
}
|
1415 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1416 |
"node_modules/vitefu": {
|
1417 |
"version": "1.1.1",
|
1418 |
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
|
@@ -1433,6 +1912,106 @@
|
|
1433 |
}
|
1434 |
}
|
1435 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1436 |
"node_modules/zimmerframe": {
|
1437 |
"version": "1.1.2",
|
1438 |
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
|
|
14 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
15 |
"@tsconfig/svelte": "^5.0.4",
|
16 |
"@types/node": "^24.0.14",
|
17 |
+
"@vitest/ui": "^3.2.4",
|
18 |
+
"fake-indexeddb": "^6.0.1",
|
19 |
+
"happy-dom": "^18.0.1",
|
20 |
"svelte": "^5.28.1",
|
21 |
"svelte-check": "^4.1.6",
|
22 |
"typescript": "~5.8.3",
|
23 |
+
"vite": "^6.3.5",
|
24 |
+
"vitest": "^3.2.4"
|
25 |
}
|
26 |
},
|
27 |
"node_modules/@ampproject/remapping": {
|
|
|
519 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
520 |
}
|
521 |
},
|
522 |
+
"node_modules/@polka/url": {
|
523 |
+
"version": "1.0.0-next.29",
|
524 |
+
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
525 |
+
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
526 |
+
"dev": true,
|
527 |
+
"license": "MIT"
|
528 |
+
},
|
529 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
530 |
"version": "4.45.1",
|
531 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
|
|
|
863 |
"dev": true,
|
864 |
"license": "MIT"
|
865 |
},
|
866 |
+
"node_modules/@types/chai": {
|
867 |
+
"version": "5.2.2",
|
868 |
+
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
|
869 |
+
"integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
|
870 |
+
"dev": true,
|
871 |
+
"license": "MIT",
|
872 |
+
"dependencies": {
|
873 |
+
"@types/deep-eql": "*"
|
874 |
+
}
|
875 |
+
},
|
876 |
+
"node_modules/@types/deep-eql": {
|
877 |
+
"version": "4.0.2",
|
878 |
+
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
879 |
+
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
880 |
+
"dev": true,
|
881 |
+
"license": "MIT"
|
882 |
+
},
|
883 |
"node_modules/@types/estree": {
|
884 |
"version": "1.0.8",
|
885 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
|
|
897 |
"undici-types": "~7.8.0"
|
898 |
}
|
899 |
},
|
900 |
+
"node_modules/@types/whatwg-mimetype": {
|
901 |
+
"version": "3.0.2",
|
902 |
+
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
903 |
+
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
904 |
+
"dev": true,
|
905 |
+
"license": "MIT"
|
906 |
+
},
|
907 |
+
"node_modules/@vitest/expect": {
|
908 |
+
"version": "3.2.4",
|
909 |
+
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
910 |
+
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
911 |
+
"dev": true,
|
912 |
+
"license": "MIT",
|
913 |
+
"dependencies": {
|
914 |
+
"@types/chai": "^5.2.2",
|
915 |
+
"@vitest/spy": "3.2.4",
|
916 |
+
"@vitest/utils": "3.2.4",
|
917 |
+
"chai": "^5.2.0",
|
918 |
+
"tinyrainbow": "^2.0.0"
|
919 |
+
},
|
920 |
+
"funding": {
|
921 |
+
"url": "https://opencollective.com/vitest"
|
922 |
+
}
|
923 |
+
},
|
924 |
+
"node_modules/@vitest/mocker": {
|
925 |
+
"version": "3.2.4",
|
926 |
+
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
927 |
+
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
928 |
+
"dev": true,
|
929 |
+
"license": "MIT",
|
930 |
+
"dependencies": {
|
931 |
+
"@vitest/spy": "3.2.4",
|
932 |
+
"estree-walker": "^3.0.3",
|
933 |
+
"magic-string": "^0.30.17"
|
934 |
+
},
|
935 |
+
"funding": {
|
936 |
+
"url": "https://opencollective.com/vitest"
|
937 |
+
},
|
938 |
+
"peerDependencies": {
|
939 |
+
"msw": "^2.4.9",
|
940 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
941 |
+
},
|
942 |
+
"peerDependenciesMeta": {
|
943 |
+
"msw": {
|
944 |
+
"optional": true
|
945 |
+
},
|
946 |
+
"vite": {
|
947 |
+
"optional": true
|
948 |
+
}
|
949 |
+
}
|
950 |
+
},
|
951 |
+
"node_modules/@vitest/pretty-format": {
|
952 |
+
"version": "3.2.4",
|
953 |
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
954 |
+
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
955 |
+
"dev": true,
|
956 |
+
"license": "MIT",
|
957 |
+
"dependencies": {
|
958 |
+
"tinyrainbow": "^2.0.0"
|
959 |
+
},
|
960 |
+
"funding": {
|
961 |
+
"url": "https://opencollective.com/vitest"
|
962 |
+
}
|
963 |
+
},
|
964 |
+
"node_modules/@vitest/runner": {
|
965 |
+
"version": "3.2.4",
|
966 |
+
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
967 |
+
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
968 |
+
"dev": true,
|
969 |
+
"license": "MIT",
|
970 |
+
"dependencies": {
|
971 |
+
"@vitest/utils": "3.2.4",
|
972 |
+
"pathe": "^2.0.3",
|
973 |
+
"strip-literal": "^3.0.0"
|
974 |
+
},
|
975 |
+
"funding": {
|
976 |
+
"url": "https://opencollective.com/vitest"
|
977 |
+
}
|
978 |
+
},
|
979 |
+
"node_modules/@vitest/snapshot": {
|
980 |
+
"version": "3.2.4",
|
981 |
+
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
982 |
+
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
983 |
+
"dev": true,
|
984 |
+
"license": "MIT",
|
985 |
+
"dependencies": {
|
986 |
+
"@vitest/pretty-format": "3.2.4",
|
987 |
+
"magic-string": "^0.30.17",
|
988 |
+
"pathe": "^2.0.3"
|
989 |
+
},
|
990 |
+
"funding": {
|
991 |
+
"url": "https://opencollective.com/vitest"
|
992 |
+
}
|
993 |
+
},
|
994 |
+
"node_modules/@vitest/spy": {
|
995 |
+
"version": "3.2.4",
|
996 |
+
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
997 |
+
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
998 |
+
"dev": true,
|
999 |
+
"license": "MIT",
|
1000 |
+
"dependencies": {
|
1001 |
+
"tinyspy": "^4.0.3"
|
1002 |
+
},
|
1003 |
+
"funding": {
|
1004 |
+
"url": "https://opencollective.com/vitest"
|
1005 |
+
}
|
1006 |
+
},
|
1007 |
+
"node_modules/@vitest/ui": {
|
1008 |
+
"version": "3.2.4",
|
1009 |
+
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz",
|
1010 |
+
"integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==",
|
1011 |
+
"dev": true,
|
1012 |
+
"license": "MIT",
|
1013 |
+
"dependencies": {
|
1014 |
+
"@vitest/utils": "3.2.4",
|
1015 |
+
"fflate": "^0.8.2",
|
1016 |
+
"flatted": "^3.3.3",
|
1017 |
+
"pathe": "^2.0.3",
|
1018 |
+
"sirv": "^3.0.1",
|
1019 |
+
"tinyglobby": "^0.2.14",
|
1020 |
+
"tinyrainbow": "^2.0.0"
|
1021 |
+
},
|
1022 |
+
"funding": {
|
1023 |
+
"url": "https://opencollective.com/vitest"
|
1024 |
+
},
|
1025 |
+
"peerDependencies": {
|
1026 |
+
"vitest": "3.2.4"
|
1027 |
+
}
|
1028 |
+
},
|
1029 |
+
"node_modules/@vitest/utils": {
|
1030 |
+
"version": "3.2.4",
|
1031 |
+
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
1032 |
+
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
1033 |
+
"dev": true,
|
1034 |
+
"license": "MIT",
|
1035 |
+
"dependencies": {
|
1036 |
+
"@vitest/pretty-format": "3.2.4",
|
1037 |
+
"loupe": "^3.1.4",
|
1038 |
+
"tinyrainbow": "^2.0.0"
|
1039 |
+
},
|
1040 |
+
"funding": {
|
1041 |
+
"url": "https://opencollective.com/vitest"
|
1042 |
+
}
|
1043 |
+
},
|
1044 |
"node_modules/acorn": {
|
1045 |
"version": "8.15.0",
|
1046 |
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
|
|
1064 |
"node": ">= 0.4"
|
1065 |
}
|
1066 |
},
|
1067 |
+
"node_modules/assertion-error": {
|
1068 |
+
"version": "2.0.1",
|
1069 |
+
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
1070 |
+
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
1071 |
+
"dev": true,
|
1072 |
+
"license": "MIT",
|
1073 |
+
"engines": {
|
1074 |
+
"node": ">=12"
|
1075 |
+
}
|
1076 |
+
},
|
1077 |
"node_modules/axobject-query": {
|
1078 |
"version": "4.1.0",
|
1079 |
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
|
|
1084 |
"node": ">= 0.4"
|
1085 |
}
|
1086 |
},
|
1087 |
+
"node_modules/cac": {
|
1088 |
+
"version": "6.7.14",
|
1089 |
+
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
1090 |
+
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
|
1091 |
+
"dev": true,
|
1092 |
+
"license": "MIT",
|
1093 |
+
"engines": {
|
1094 |
+
"node": ">=8"
|
1095 |
+
}
|
1096 |
+
},
|
1097 |
+
"node_modules/chai": {
|
1098 |
+
"version": "5.2.1",
|
1099 |
+
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz",
|
1100 |
+
"integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==",
|
1101 |
+
"dev": true,
|
1102 |
+
"license": "MIT",
|
1103 |
+
"dependencies": {
|
1104 |
+
"assertion-error": "^2.0.1",
|
1105 |
+
"check-error": "^2.1.1",
|
1106 |
+
"deep-eql": "^5.0.1",
|
1107 |
+
"loupe": "^3.1.0",
|
1108 |
+
"pathval": "^2.0.0"
|
1109 |
+
},
|
1110 |
+
"engines": {
|
1111 |
+
"node": ">=18"
|
1112 |
+
}
|
1113 |
+
},
|
1114 |
+
"node_modules/check-error": {
|
1115 |
+
"version": "2.1.1",
|
1116 |
+
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
1117 |
+
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
1118 |
+
"dev": true,
|
1119 |
+
"license": "MIT",
|
1120 |
+
"engines": {
|
1121 |
+
"node": ">= 16"
|
1122 |
+
}
|
1123 |
+
},
|
1124 |
"node_modules/chokidar": {
|
1125 |
"version": "4.0.3",
|
1126 |
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
|
|
1165 |
}
|
1166 |
}
|
1167 |
},
|
1168 |
+
"node_modules/deep-eql": {
|
1169 |
+
"version": "5.0.2",
|
1170 |
+
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
1171 |
+
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
|
1172 |
+
"dev": true,
|
1173 |
+
"license": "MIT",
|
1174 |
+
"engines": {
|
1175 |
+
"node": ">=6"
|
1176 |
+
}
|
1177 |
+
},
|
1178 |
"node_modules/deepmerge": {
|
1179 |
"version": "4.3.1",
|
1180 |
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
|
|
1191 |
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
1192 |
"license": "Apache-2.0"
|
1193 |
},
|
1194 |
+
"node_modules/es-module-lexer": {
|
1195 |
+
"version": "1.7.0",
|
1196 |
+
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
1197 |
+
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
1198 |
+
"dev": true,
|
1199 |
+
"license": "MIT"
|
1200 |
+
},
|
1201 |
"node_modules/esbuild": {
|
1202 |
"version": "0.25.6",
|
1203 |
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
|
|
1257 |
"@jridgewell/sourcemap-codec": "^1.4.15"
|
1258 |
}
|
1259 |
},
|
1260 |
+
"node_modules/estree-walker": {
|
1261 |
+
"version": "3.0.3",
|
1262 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
1263 |
+
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
1264 |
+
"dev": true,
|
1265 |
+
"license": "MIT",
|
1266 |
+
"dependencies": {
|
1267 |
+
"@types/estree": "^1.0.0"
|
1268 |
+
}
|
1269 |
+
},
|
1270 |
+
"node_modules/expect-type": {
|
1271 |
+
"version": "1.2.2",
|
1272 |
+
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
1273 |
+
"integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
|
1274 |
+
"dev": true,
|
1275 |
+
"license": "Apache-2.0",
|
1276 |
+
"engines": {
|
1277 |
+
"node": ">=12.0.0"
|
1278 |
+
}
|
1279 |
+
},
|
1280 |
+
"node_modules/fake-indexeddb": {
|
1281 |
+
"version": "6.0.1",
|
1282 |
+
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.1.tgz",
|
1283 |
+
"integrity": "sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ==",
|
1284 |
+
"dev": true,
|
1285 |
+
"license": "Apache-2.0",
|
1286 |
+
"engines": {
|
1287 |
+
"node": ">=18"
|
1288 |
+
}
|
1289 |
+
},
|
1290 |
"node_modules/fdir": {
|
1291 |
"version": "6.4.6",
|
1292 |
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
|
|
1302 |
}
|
1303 |
}
|
1304 |
},
|
1305 |
+
"node_modules/fflate": {
|
1306 |
+
"version": "0.8.2",
|
1307 |
+
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
1308 |
+
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
1309 |
+
"dev": true,
|
1310 |
+
"license": "MIT"
|
1311 |
+
},
|
1312 |
+
"node_modules/flatted": {
|
1313 |
+
"version": "3.3.3",
|
1314 |
+
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
1315 |
+
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
1316 |
+
"dev": true,
|
1317 |
+
"license": "ISC"
|
1318 |
+
},
|
1319 |
"node_modules/fsevents": {
|
1320 |
"version": "2.3.3",
|
1321 |
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
|
1331 |
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
1332 |
}
|
1333 |
},
|
1334 |
+
"node_modules/happy-dom": {
|
1335 |
+
"version": "18.0.1",
|
1336 |
+
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz",
|
1337 |
+
"integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==",
|
1338 |
+
"dev": true,
|
1339 |
+
"license": "MIT",
|
1340 |
+
"dependencies": {
|
1341 |
+
"@types/node": "^20.0.0",
|
1342 |
+
"@types/whatwg-mimetype": "^3.0.2",
|
1343 |
+
"whatwg-mimetype": "^3.0.0"
|
1344 |
+
},
|
1345 |
+
"engines": {
|
1346 |
+
"node": ">=20.0.0"
|
1347 |
+
}
|
1348 |
+
},
|
1349 |
+
"node_modules/happy-dom/node_modules/@types/node": {
|
1350 |
+
"version": "20.19.9",
|
1351 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
|
1352 |
+
"integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==",
|
1353 |
+
"dev": true,
|
1354 |
+
"license": "MIT",
|
1355 |
+
"dependencies": {
|
1356 |
+
"undici-types": "~6.21.0"
|
1357 |
+
}
|
1358 |
+
},
|
1359 |
+
"node_modules/happy-dom/node_modules/undici-types": {
|
1360 |
+
"version": "6.21.0",
|
1361 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
1362 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
1363 |
+
"dev": true,
|
1364 |
+
"license": "MIT"
|
1365 |
+
},
|
1366 |
"node_modules/is-reference": {
|
1367 |
"version": "3.0.3",
|
1368 |
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
|
|
1373 |
"@types/estree": "^1.0.6"
|
1374 |
}
|
1375 |
},
|
1376 |
+
"node_modules/js-tokens": {
|
1377 |
+
"version": "9.0.1",
|
1378 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
|
1379 |
+
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
|
1380 |
+
"dev": true,
|
1381 |
+
"license": "MIT"
|
1382 |
+
},
|
1383 |
"node_modules/kleur": {
|
1384 |
"version": "4.1.5",
|
1385 |
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
|
|
1397 |
"dev": true,
|
1398 |
"license": "MIT"
|
1399 |
},
|
1400 |
+
"node_modules/loupe": {
|
1401 |
+
"version": "3.1.4",
|
1402 |
+
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
|
1403 |
+
"integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
|
1404 |
+
"dev": true,
|
1405 |
+
"license": "MIT"
|
1406 |
+
},
|
1407 |
"node_modules/magic-string": {
|
1408 |
"version": "0.30.17",
|
1409 |
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
|
|
1424 |
"node": ">=4"
|
1425 |
}
|
1426 |
},
|
1427 |
+
"node_modules/mrmime": {
|
1428 |
+
"version": "2.0.1",
|
1429 |
+
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
1430 |
+
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
1431 |
+
"dev": true,
|
1432 |
+
"license": "MIT",
|
1433 |
+
"engines": {
|
1434 |
+
"node": ">=10"
|
1435 |
+
}
|
1436 |
+
},
|
1437 |
"node_modules/ms": {
|
1438 |
"version": "2.1.3",
|
1439 |
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
|
1460 |
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
1461 |
}
|
1462 |
},
|
1463 |
+
"node_modules/pathe": {
|
1464 |
+
"version": "2.0.3",
|
1465 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
1466 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
1467 |
+
"dev": true,
|
1468 |
+
"license": "MIT"
|
1469 |
+
},
|
1470 |
+
"node_modules/pathval": {
|
1471 |
+
"version": "2.0.1",
|
1472 |
+
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
|
1473 |
+
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
|
1474 |
+
"dev": true,
|
1475 |
+
"license": "MIT",
|
1476 |
+
"engines": {
|
1477 |
+
"node": ">= 14.16"
|
1478 |
+
}
|
1479 |
+
},
|
1480 |
"node_modules/picocolors": {
|
1481 |
"version": "1.1.1",
|
1482 |
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
|
1593 |
"node": ">=6"
|
1594 |
}
|
1595 |
},
|
1596 |
+
"node_modules/siginfo": {
|
1597 |
+
"version": "2.0.0",
|
1598 |
+
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
1599 |
+
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
1600 |
+
"dev": true,
|
1601 |
+
"license": "ISC"
|
1602 |
+
},
|
1603 |
+
"node_modules/sirv": {
|
1604 |
+
"version": "3.0.1",
|
1605 |
+
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
|
1606 |
+
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
|
1607 |
+
"dev": true,
|
1608 |
+
"license": "MIT",
|
1609 |
+
"dependencies": {
|
1610 |
+
"@polka/url": "^1.0.0-next.24",
|
1611 |
+
"mrmime": "^2.0.0",
|
1612 |
+
"totalist": "^3.0.0"
|
1613 |
+
},
|
1614 |
+
"engines": {
|
1615 |
+
"node": ">=18"
|
1616 |
+
}
|
1617 |
+
},
|
1618 |
"node_modules/source-map-js": {
|
1619 |
"version": "1.2.1",
|
1620 |
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
|
1625 |
"node": ">=0.10.0"
|
1626 |
}
|
1627 |
},
|
1628 |
+
"node_modules/stackback": {
|
1629 |
+
"version": "0.0.2",
|
1630 |
+
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
1631 |
+
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
1632 |
+
"dev": true,
|
1633 |
+
"license": "MIT"
|
1634 |
+
},
|
1635 |
+
"node_modules/std-env": {
|
1636 |
+
"version": "3.9.0",
|
1637 |
+
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
|
1638 |
+
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
|
1639 |
+
"dev": true,
|
1640 |
+
"license": "MIT"
|
1641 |
+
},
|
1642 |
+
"node_modules/strip-literal": {
|
1643 |
+
"version": "3.0.0",
|
1644 |
+
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
|
1645 |
+
"integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
|
1646 |
+
"dev": true,
|
1647 |
+
"license": "MIT",
|
1648 |
+
"dependencies": {
|
1649 |
+
"js-tokens": "^9.0.1"
|
1650 |
+
},
|
1651 |
+
"funding": {
|
1652 |
+
"url": "https://github.com/sponsors/antfu"
|
1653 |
+
}
|
1654 |
+
},
|
1655 |
"node_modules/svelte": {
|
1656 |
"version": "5.36.1",
|
1657 |
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.36.1.tgz",
|
|
|
1702 |
"typescript": ">=5.0.0"
|
1703 |
}
|
1704 |
},
|
1705 |
+
"node_modules/tinybench": {
|
1706 |
+
"version": "2.9.0",
|
1707 |
+
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
1708 |
+
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
1709 |
+
"dev": true,
|
1710 |
+
"license": "MIT"
|
1711 |
+
},
|
1712 |
+
"node_modules/tinyexec": {
|
1713 |
+
"version": "0.3.2",
|
1714 |
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
1715 |
+
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
|
1716 |
+
"dev": true,
|
1717 |
+
"license": "MIT"
|
1718 |
+
},
|
1719 |
"node_modules/tinyglobby": {
|
1720 |
"version": "0.2.14",
|
1721 |
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
|
|
1733 |
"url": "https://github.com/sponsors/SuperchupuDev"
|
1734 |
}
|
1735 |
},
|
1736 |
+
"node_modules/tinypool": {
|
1737 |
+
"version": "1.1.1",
|
1738 |
+
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
|
1739 |
+
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
|
1740 |
+
"dev": true,
|
1741 |
+
"license": "MIT",
|
1742 |
+
"engines": {
|
1743 |
+
"node": "^18.0.0 || >=20.0.0"
|
1744 |
+
}
|
1745 |
+
},
|
1746 |
+
"node_modules/tinyrainbow": {
|
1747 |
+
"version": "2.0.0",
|
1748 |
+
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
|
1749 |
+
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
|
1750 |
+
"dev": true,
|
1751 |
+
"license": "MIT",
|
1752 |
+
"engines": {
|
1753 |
+
"node": ">=14.0.0"
|
1754 |
+
}
|
1755 |
+
},
|
1756 |
+
"node_modules/tinyspy": {
|
1757 |
+
"version": "4.0.3",
|
1758 |
+
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
|
1759 |
+
"integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
|
1760 |
+
"dev": true,
|
1761 |
+
"license": "MIT",
|
1762 |
+
"engines": {
|
1763 |
+
"node": ">=14.0.0"
|
1764 |
+
}
|
1765 |
+
},
|
1766 |
+
"node_modules/totalist": {
|
1767 |
+
"version": "3.0.1",
|
1768 |
+
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
1769 |
+
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
1770 |
+
"dev": true,
|
1771 |
+
"license": "MIT",
|
1772 |
+
"engines": {
|
1773 |
+
"node": ">=6"
|
1774 |
+
}
|
1775 |
+
},
|
1776 |
"node_modules/typescript": {
|
1777 |
"version": "5.8.3",
|
1778 |
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
|
|
1869 |
}
|
1870 |
}
|
1871 |
},
|
1872 |
+
"node_modules/vite-node": {
|
1873 |
+
"version": "3.2.4",
|
1874 |
+
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
|
1875 |
+
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
1876 |
+
"dev": true,
|
1877 |
+
"license": "MIT",
|
1878 |
+
"dependencies": {
|
1879 |
+
"cac": "^6.7.14",
|
1880 |
+
"debug": "^4.4.1",
|
1881 |
+
"es-module-lexer": "^1.7.0",
|
1882 |
+
"pathe": "^2.0.3",
|
1883 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
|
1884 |
+
},
|
1885 |
+
"bin": {
|
1886 |
+
"vite-node": "vite-node.mjs"
|
1887 |
+
},
|
1888 |
+
"engines": {
|
1889 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
1890 |
+
},
|
1891 |
+
"funding": {
|
1892 |
+
"url": "https://opencollective.com/vitest"
|
1893 |
+
}
|
1894 |
+
},
|
1895 |
"node_modules/vitefu": {
|
1896 |
"version": "1.1.1",
|
1897 |
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
|
|
|
1912 |
}
|
1913 |
}
|
1914 |
},
|
1915 |
+
"node_modules/vitest": {
|
1916 |
+
"version": "3.2.4",
|
1917 |
+
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
1918 |
+
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
1919 |
+
"dev": true,
|
1920 |
+
"license": "MIT",
|
1921 |
+
"dependencies": {
|
1922 |
+
"@types/chai": "^5.2.2",
|
1923 |
+
"@vitest/expect": "3.2.4",
|
1924 |
+
"@vitest/mocker": "3.2.4",
|
1925 |
+
"@vitest/pretty-format": "^3.2.4",
|
1926 |
+
"@vitest/runner": "3.2.4",
|
1927 |
+
"@vitest/snapshot": "3.2.4",
|
1928 |
+
"@vitest/spy": "3.2.4",
|
1929 |
+
"@vitest/utils": "3.2.4",
|
1930 |
+
"chai": "^5.2.0",
|
1931 |
+
"debug": "^4.4.1",
|
1932 |
+
"expect-type": "^1.2.1",
|
1933 |
+
"magic-string": "^0.30.17",
|
1934 |
+
"pathe": "^2.0.3",
|
1935 |
+
"picomatch": "^4.0.2",
|
1936 |
+
"std-env": "^3.9.0",
|
1937 |
+
"tinybench": "^2.9.0",
|
1938 |
+
"tinyexec": "^0.3.2",
|
1939 |
+
"tinyglobby": "^0.2.14",
|
1940 |
+
"tinypool": "^1.1.1",
|
1941 |
+
"tinyrainbow": "^2.0.0",
|
1942 |
+
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
|
1943 |
+
"vite-node": "3.2.4",
|
1944 |
+
"why-is-node-running": "^2.3.0"
|
1945 |
+
},
|
1946 |
+
"bin": {
|
1947 |
+
"vitest": "vitest.mjs"
|
1948 |
+
},
|
1949 |
+
"engines": {
|
1950 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
1951 |
+
},
|
1952 |
+
"funding": {
|
1953 |
+
"url": "https://opencollective.com/vitest"
|
1954 |
+
},
|
1955 |
+
"peerDependencies": {
|
1956 |
+
"@edge-runtime/vm": "*",
|
1957 |
+
"@types/debug": "^4.1.12",
|
1958 |
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
1959 |
+
"@vitest/browser": "3.2.4",
|
1960 |
+
"@vitest/ui": "3.2.4",
|
1961 |
+
"happy-dom": "*",
|
1962 |
+
"jsdom": "*"
|
1963 |
+
},
|
1964 |
+
"peerDependenciesMeta": {
|
1965 |
+
"@edge-runtime/vm": {
|
1966 |
+
"optional": true
|
1967 |
+
},
|
1968 |
+
"@types/debug": {
|
1969 |
+
"optional": true
|
1970 |
+
},
|
1971 |
+
"@types/node": {
|
1972 |
+
"optional": true
|
1973 |
+
},
|
1974 |
+
"@vitest/browser": {
|
1975 |
+
"optional": true
|
1976 |
+
},
|
1977 |
+
"@vitest/ui": {
|
1978 |
+
"optional": true
|
1979 |
+
},
|
1980 |
+
"happy-dom": {
|
1981 |
+
"optional": true
|
1982 |
+
},
|
1983 |
+
"jsdom": {
|
1984 |
+
"optional": true
|
1985 |
+
}
|
1986 |
+
}
|
1987 |
+
},
|
1988 |
+
"node_modules/whatwg-mimetype": {
|
1989 |
+
"version": "3.0.0",
|
1990 |
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
1991 |
+
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
1992 |
+
"dev": true,
|
1993 |
+
"license": "MIT",
|
1994 |
+
"engines": {
|
1995 |
+
"node": ">=12"
|
1996 |
+
}
|
1997 |
+
},
|
1998 |
+
"node_modules/why-is-node-running": {
|
1999 |
+
"version": "2.3.0",
|
2000 |
+
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
2001 |
+
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
2002 |
+
"dev": true,
|
2003 |
+
"license": "MIT",
|
2004 |
+
"dependencies": {
|
2005 |
+
"siginfo": "^2.0.0",
|
2006 |
+
"stackback": "0.0.2"
|
2007 |
+
},
|
2008 |
+
"bin": {
|
2009 |
+
"why-is-node-running": "cli.js"
|
2010 |
+
},
|
2011 |
+
"engines": {
|
2012 |
+
"node": ">=8"
|
2013 |
+
}
|
2014 |
+
},
|
2015 |
"node_modules/zimmerframe": {
|
2016 |
"version": "1.1.2",
|
2017 |
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
package.json
CHANGED
@@ -7,16 +7,22 @@
|
|
7 |
"dev": "vite",
|
8 |
"build": "vite build",
|
9 |
"preview": "vite preview",
|
10 |
-
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
|
|
|
|
11 |
},
|
12 |
"devDependencies": {
|
13 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
14 |
"@tsconfig/svelte": "^5.0.4",
|
15 |
"@types/node": "^24.0.14",
|
|
|
|
|
|
|
16 |
"svelte": "^5.28.1",
|
17 |
"svelte-check": "^4.1.6",
|
18 |
"typescript": "~5.8.3",
|
19 |
-
"vite": "^6.3.5"
|
|
|
20 |
},
|
21 |
"dependencies": {
|
22 |
"dexie": "^4.0.11"
|
|
|
7 |
"dev": "vite",
|
8 |
"build": "vite build",
|
9 |
"preview": "vite preview",
|
10 |
+
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
11 |
+
"test": "vitest",
|
12 |
+
"test:ui": "vitest --ui"
|
13 |
},
|
14 |
"devDependencies": {
|
15 |
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
16 |
"@tsconfig/svelte": "^5.0.4",
|
17 |
"@types/node": "^24.0.14",
|
18 |
+
"@vitest/ui": "^3.2.4",
|
19 |
+
"fake-indexeddb": "^6.0.1",
|
20 |
+
"happy-dom": "^18.0.1",
|
21 |
"svelte": "^5.28.1",
|
22 |
"svelte-check": "^4.1.6",
|
23 |
"typescript": "~5.8.3",
|
24 |
+
"vite": "^6.3.5",
|
25 |
+
"vitest": "^3.2.4"
|
26 |
},
|
27 |
"dependencies": {
|
28 |
"dexie": "^4.0.11"
|
public/images/default_trainer.png
ADDED
![]() |
Git LFS Details
|
public/images/environments/cave.png
ADDED
![]() |
Git LFS Details
|
public/images/environments/dinner.png
ADDED
![]() |
Git LFS Details
|
public/images/environments/road.png
ADDED
![]() |
Git LFS Details
|
public/images/environments/ruins.png
ADDED
![]() |
Git LFS Details
|
src/lib/components/Battle/ActionButtons.svelte
ADDED
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
export let isWildBattle: boolean;
|
3 |
+
export let onAction: (action: string) => void;
|
4 |
+
</script>
|
5 |
+
|
6 |
+
<div class="action-grid">
|
7 |
+
<button class="action-button fight" on:click={() => onAction('fight')}>
|
8 |
+
<span class="action-icon">⚔️</span>
|
9 |
+
<span class="action-label">Fight</span>
|
10 |
+
</button>
|
11 |
+
|
12 |
+
<button class="action-button piclet" on:click={() => onAction('piclet')}>
|
13 |
+
<span class="action-icon">🔄</span>
|
14 |
+
<span class="action-label">Piclet</span>
|
15 |
+
</button>
|
16 |
+
|
17 |
+
{#if isWildBattle}
|
18 |
+
<button class="action-button catch" on:click={() => onAction('catch')}>
|
19 |
+
<span class="action-icon">🎯</span>
|
20 |
+
<span class="action-label">Catch</span>
|
21 |
+
</button>
|
22 |
+
{:else}
|
23 |
+
<button class="action-button items" disabled>
|
24 |
+
<span class="action-icon">🎒</span>
|
25 |
+
<span class="action-label">Items</span>
|
26 |
+
</button>
|
27 |
+
{/if}
|
28 |
+
|
29 |
+
<button class="action-button run" on:click={() => onAction('run')}>
|
30 |
+
<span class="action-icon">🏃</span>
|
31 |
+
<span class="action-label">Run</span>
|
32 |
+
</button>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<style>
|
36 |
+
.action-grid {
|
37 |
+
display: grid;
|
38 |
+
grid-template-columns: repeat(2, 1fr);
|
39 |
+
gap: 1rem;
|
40 |
+
max-width: 400px;
|
41 |
+
margin: 0 auto;
|
42 |
+
width: 100%;
|
43 |
+
}
|
44 |
+
|
45 |
+
.action-button {
|
46 |
+
padding: 2rem 1rem;
|
47 |
+
background: #f8f9fa;
|
48 |
+
border: 2px solid #e0e0e0;
|
49 |
+
border-radius: 12px;
|
50 |
+
cursor: pointer;
|
51 |
+
display: flex;
|
52 |
+
flex-direction: column;
|
53 |
+
align-items: center;
|
54 |
+
gap: 0.5rem;
|
55 |
+
transition: all 0.2s ease;
|
56 |
+
position: relative;
|
57 |
+
overflow: hidden;
|
58 |
+
}
|
59 |
+
|
60 |
+
.action-button:hover:not(:disabled) {
|
61 |
+
transform: translateY(-2px);
|
62 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
63 |
+
}
|
64 |
+
|
65 |
+
.action-button:active:not(:disabled) {
|
66 |
+
transform: translateY(0);
|
67 |
+
}
|
68 |
+
|
69 |
+
.action-button:disabled {
|
70 |
+
opacity: 0.5;
|
71 |
+
cursor: not-allowed;
|
72 |
+
}
|
73 |
+
|
74 |
+
.action-icon {
|
75 |
+
font-size: 2rem;
|
76 |
+
}
|
77 |
+
|
78 |
+
.action-label {
|
79 |
+
font-weight: 600;
|
80 |
+
font-size: 1rem;
|
81 |
+
color: #333;
|
82 |
+
}
|
83 |
+
|
84 |
+
/* Action specific colors */
|
85 |
+
.fight {
|
86 |
+
background: #ffebee;
|
87 |
+
border-color: #ef5350;
|
88 |
+
}
|
89 |
+
|
90 |
+
.fight:hover:not(:disabled) {
|
91 |
+
background: #ffcdd2;
|
92 |
+
border-color: #e53935;
|
93 |
+
}
|
94 |
+
|
95 |
+
.piclet {
|
96 |
+
background: #e3f2fd;
|
97 |
+
border-color: #42a5f5;
|
98 |
+
}
|
99 |
+
|
100 |
+
.piclet:hover:not(:disabled) {
|
101 |
+
background: #bbdefb;
|
102 |
+
border-color: #2196f3;
|
103 |
+
}
|
104 |
+
|
105 |
+
.catch {
|
106 |
+
background: #e8f5e9;
|
107 |
+
border-color: #66bb6a;
|
108 |
+
}
|
109 |
+
|
110 |
+
.catch:hover:not(:disabled) {
|
111 |
+
background: #c8e6c9;
|
112 |
+
border-color: #4caf50;
|
113 |
+
}
|
114 |
+
|
115 |
+
.items {
|
116 |
+
background: #fff3e0;
|
117 |
+
border-color: #ffa726;
|
118 |
+
}
|
119 |
+
|
120 |
+
.items:hover:not(:disabled) {
|
121 |
+
background: #ffe0b2;
|
122 |
+
border-color: #ff9800;
|
123 |
+
}
|
124 |
+
|
125 |
+
.run {
|
126 |
+
background: #f3e5f5;
|
127 |
+
border-color: #ab47bc;
|
128 |
+
}
|
129 |
+
|
130 |
+
.run:hover:not(:disabled) {
|
131 |
+
background: #e1bee7;
|
132 |
+
border-color: #9c27b0;
|
133 |
+
}
|
134 |
+
|
135 |
+
@media (max-width: 480px) {
|
136 |
+
.action-button {
|
137 |
+
padding: 1.5rem 0.75rem;
|
138 |
+
}
|
139 |
+
|
140 |
+
.action-icon {
|
141 |
+
font-size: 1.5rem;
|
142 |
+
}
|
143 |
+
|
144 |
+
.action-label {
|
145 |
+
font-size: 0.875rem;
|
146 |
+
}
|
147 |
+
}
|
148 |
+
</style>
|
src/lib/components/Battle/BattleControls.svelte
ADDED
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { PicletInstance } from '$lib/db/schema';
|
3 |
+
import { db } from '$lib/db';
|
4 |
+
import ActionButtons from './ActionButtons.svelte';
|
5 |
+
|
6 |
+
export let currentMessage: string;
|
7 |
+
export let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended';
|
8 |
+
export let processingTurn: boolean;
|
9 |
+
export let battleEnded: boolean;
|
10 |
+
export let isWildBattle: boolean;
|
11 |
+
export let playerPiclet: PicletInstance;
|
12 |
+
export let onAction: (action: string) => void;
|
13 |
+
export let onMoveSelect: (move: any) => void;
|
14 |
+
export let onPicletSelect: (piclet: PicletInstance) => void;
|
15 |
+
export let onBack: () => void;
|
16 |
+
|
17 |
+
let availablePiclets: PicletInstance[] = [];
|
18 |
+
|
19 |
+
// Load player's roster
|
20 |
+
async function loadRoster() {
|
21 |
+
const roster = await db.picletInstances
|
22 |
+
.where('isInRoster')
|
23 |
+
.equals(1)
|
24 |
+
.toArray();
|
25 |
+
availablePiclets = roster.filter(p => p.currentHp > 0 && p.id !== playerPiclet.id);
|
26 |
+
}
|
27 |
+
|
28 |
+
$: if (battlePhase === 'picletSelect') {
|
29 |
+
loadRoster();
|
30 |
+
}
|
31 |
+
</script>
|
32 |
+
|
33 |
+
<div class="battle-controls">
|
34 |
+
<!-- Message Bar -->
|
35 |
+
<div class="message-bar {battleEnded ? 'special' : ''}">
|
36 |
+
<p>{currentMessage}</p>
|
37 |
+
</div>
|
38 |
+
|
39 |
+
<!-- Action Area -->
|
40 |
+
<div class="action-area">
|
41 |
+
{#if battlePhase === 'main' && !processingTurn && !battleEnded}
|
42 |
+
<ActionButtons
|
43 |
+
{isWildBattle}
|
44 |
+
{onAction}
|
45 |
+
/>
|
46 |
+
{:else if battlePhase === 'moveSelect'}
|
47 |
+
<div class="move-select">
|
48 |
+
<div class="section-header">
|
49 |
+
<h3>Select a move</h3>
|
50 |
+
<button class="back-btn" on:click={onBack}>← Back</button>
|
51 |
+
</div>
|
52 |
+
<div class="moves-grid">
|
53 |
+
{#each playerPiclet.moves as move}
|
54 |
+
<button
|
55 |
+
class="move-button"
|
56 |
+
on:click={() => onMoveSelect(move)}
|
57 |
+
disabled={move.currentPp <= 0}
|
58 |
+
>
|
59 |
+
<span class="move-name">{move.name}</span>
|
60 |
+
<span class="move-type">{move.type}</span>
|
61 |
+
<span class="move-pp">PP: {move.currentPp}/{move.pp}</span>
|
62 |
+
</button>
|
63 |
+
{/each}
|
64 |
+
</div>
|
65 |
+
</div>
|
66 |
+
{:else if battlePhase === 'picletSelect'}
|
67 |
+
<div class="piclet-select">
|
68 |
+
<div class="section-header">
|
69 |
+
<h3>Select a Piclet</h3>
|
70 |
+
<button class="back-btn" on:click={onBack}>← Back</button>
|
71 |
+
</div>
|
72 |
+
<div class="piclets-list">
|
73 |
+
{#if availablePiclets.length === 0}
|
74 |
+
<p class="no-piclets">No other healthy Piclets available!</p>
|
75 |
+
{:else}
|
76 |
+
{#each availablePiclets as piclet}
|
77 |
+
<button
|
78 |
+
class="piclet-option"
|
79 |
+
on:click={() => onPicletSelect(piclet)}
|
80 |
+
>
|
81 |
+
<img
|
82 |
+
src={piclet.imageUrl}
|
83 |
+
alt={piclet.nickname}
|
84 |
+
on:error={(e) => e.currentTarget.src = 'https://via.placeholder.com/50x50?text=P'}
|
85 |
+
/>
|
86 |
+
<div class="piclet-details">
|
87 |
+
<span class="piclet-name">{piclet.nickname}</span>
|
88 |
+
<span class="piclet-stats">Lv.{piclet.level} - HP: {piclet.currentHp}/{piclet.maxHp}</span>
|
89 |
+
</div>
|
90 |
+
<div class="hp-preview">
|
91 |
+
<div class="hp-preview-bar">
|
92 |
+
<div
|
93 |
+
class="hp-preview-fill"
|
94 |
+
style="width: {(piclet.currentHp / piclet.maxHp) * 100}%"
|
95 |
+
></div>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</button>
|
99 |
+
{/each}
|
100 |
+
{/if}
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
{:else if battleEnded}
|
104 |
+
<div class="battle-end">
|
105 |
+
<button class="continue-btn" on:click={() => window.history.back()}>
|
106 |
+
Continue
|
107 |
+
</button>
|
108 |
+
</div>
|
109 |
+
{/if}
|
110 |
+
</div>
|
111 |
+
</div>
|
112 |
+
|
113 |
+
<style>
|
114 |
+
.battle-controls {
|
115 |
+
flex: 1;
|
116 |
+
display: flex;
|
117 |
+
flex-direction: column;
|
118 |
+
background: white;
|
119 |
+
border-top: 1px solid #e0e0e0;
|
120 |
+
}
|
121 |
+
|
122 |
+
.message-bar {
|
123 |
+
padding: 1rem;
|
124 |
+
background: #f8f9fa;
|
125 |
+
border-bottom: 1px solid #e0e0e0;
|
126 |
+
text-align: center;
|
127 |
+
}
|
128 |
+
|
129 |
+
.message-bar.special {
|
130 |
+
background: rgba(255, 152, 0, 0.1);
|
131 |
+
border-color: rgba(255, 152, 0, 0.3);
|
132 |
+
}
|
133 |
+
|
134 |
+
.message-bar p {
|
135 |
+
margin: 0;
|
136 |
+
font-size: 1rem;
|
137 |
+
color: #333;
|
138 |
+
}
|
139 |
+
|
140 |
+
.action-area {
|
141 |
+
flex: 1;
|
142 |
+
padding: 1rem;
|
143 |
+
display: flex;
|
144 |
+
flex-direction: column;
|
145 |
+
}
|
146 |
+
|
147 |
+
/* Move Selection */
|
148 |
+
.move-select, .piclet-select {
|
149 |
+
display: flex;
|
150 |
+
flex-direction: column;
|
151 |
+
height: 100%;
|
152 |
+
}
|
153 |
+
|
154 |
+
.section-header {
|
155 |
+
display: flex;
|
156 |
+
justify-content: space-between;
|
157 |
+
align-items: center;
|
158 |
+
margin-bottom: 1rem;
|
159 |
+
}
|
160 |
+
|
161 |
+
.section-header h3 {
|
162 |
+
margin: 0;
|
163 |
+
font-size: 1.125rem;
|
164 |
+
color: #1a1a1a;
|
165 |
+
}
|
166 |
+
|
167 |
+
.back-btn {
|
168 |
+
background: none;
|
169 |
+
border: none;
|
170 |
+
color: #007bff;
|
171 |
+
font-size: 0.875rem;
|
172 |
+
cursor: pointer;
|
173 |
+
padding: 0.25rem 0.5rem;
|
174 |
+
}
|
175 |
+
|
176 |
+
.moves-grid {
|
177 |
+
display: grid;
|
178 |
+
grid-template-columns: repeat(2, 1fr);
|
179 |
+
gap: 0.75rem;
|
180 |
+
}
|
181 |
+
|
182 |
+
.move-button {
|
183 |
+
padding: 1rem;
|
184 |
+
background: #f8f9fa;
|
185 |
+
border: 2px solid #e0e0e0;
|
186 |
+
border-radius: 8px;
|
187 |
+
cursor: pointer;
|
188 |
+
display: flex;
|
189 |
+
flex-direction: column;
|
190 |
+
align-items: flex-start;
|
191 |
+
gap: 0.25rem;
|
192 |
+
transition: all 0.2s ease;
|
193 |
+
}
|
194 |
+
|
195 |
+
.move-button:hover:not(:disabled) {
|
196 |
+
background: #e9ecef;
|
197 |
+
border-color: #007bff;
|
198 |
+
}
|
199 |
+
|
200 |
+
.move-button:disabled {
|
201 |
+
opacity: 0.5;
|
202 |
+
cursor: not-allowed;
|
203 |
+
}
|
204 |
+
|
205 |
+
.move-name {
|
206 |
+
font-weight: 600;
|
207 |
+
color: #1a1a1a;
|
208 |
+
}
|
209 |
+
|
210 |
+
.move-type {
|
211 |
+
font-size: 0.75rem;
|
212 |
+
color: #666;
|
213 |
+
text-transform: uppercase;
|
214 |
+
}
|
215 |
+
|
216 |
+
.move-pp {
|
217 |
+
font-size: 0.75rem;
|
218 |
+
color: #999;
|
219 |
+
}
|
220 |
+
|
221 |
+
/* Piclet Selection */
|
222 |
+
.piclets-list {
|
223 |
+
flex: 1;
|
224 |
+
overflow-y: auto;
|
225 |
+
display: flex;
|
226 |
+
flex-direction: column;
|
227 |
+
gap: 0.5rem;
|
228 |
+
}
|
229 |
+
|
230 |
+
.no-piclets {
|
231 |
+
text-align: center;
|
232 |
+
color: #666;
|
233 |
+
padding: 2rem;
|
234 |
+
}
|
235 |
+
|
236 |
+
.piclet-option {
|
237 |
+
display: flex;
|
238 |
+
align-items: center;
|
239 |
+
gap: 1rem;
|
240 |
+
padding: 0.75rem;
|
241 |
+
background: #f8f9fa;
|
242 |
+
border: 2px solid #e0e0e0;
|
243 |
+
border-radius: 8px;
|
244 |
+
cursor: pointer;
|
245 |
+
transition: all 0.2s ease;
|
246 |
+
}
|
247 |
+
|
248 |
+
.piclet-option:hover {
|
249 |
+
background: #e9ecef;
|
250 |
+
border-color: #007bff;
|
251 |
+
}
|
252 |
+
|
253 |
+
.piclet-option img {
|
254 |
+
width: 50px;
|
255 |
+
height: 50px;
|
256 |
+
object-fit: cover;
|
257 |
+
border-radius: 4px;
|
258 |
+
}
|
259 |
+
|
260 |
+
.piclet-details {
|
261 |
+
flex: 1;
|
262 |
+
display: flex;
|
263 |
+
flex-direction: column;
|
264 |
+
align-items: flex-start;
|
265 |
+
}
|
266 |
+
|
267 |
+
.piclet-name {
|
268 |
+
font-weight: 600;
|
269 |
+
color: #1a1a1a;
|
270 |
+
}
|
271 |
+
|
272 |
+
.piclet-stats {
|
273 |
+
font-size: 0.75rem;
|
274 |
+
color: #666;
|
275 |
+
}
|
276 |
+
|
277 |
+
.hp-preview {
|
278 |
+
width: 80px;
|
279 |
+
}
|
280 |
+
|
281 |
+
.hp-preview-bar {
|
282 |
+
height: 6px;
|
283 |
+
background: #e0e0e0;
|
284 |
+
border-radius: 3px;
|
285 |
+
overflow: hidden;
|
286 |
+
}
|
287 |
+
|
288 |
+
.hp-preview-fill {
|
289 |
+
height: 100%;
|
290 |
+
background: #4caf50;
|
291 |
+
transition: width 0.3s ease;
|
292 |
+
}
|
293 |
+
|
294 |
+
/* Battle End */
|
295 |
+
.battle-end {
|
296 |
+
display: flex;
|
297 |
+
justify-content: center;
|
298 |
+
align-items: center;
|
299 |
+
height: 100%;
|
300 |
+
}
|
301 |
+
|
302 |
+
.continue-btn {
|
303 |
+
padding: 1rem 2rem;
|
304 |
+
background: #007bff;
|
305 |
+
color: white;
|
306 |
+
border: none;
|
307 |
+
border-radius: 8px;
|
308 |
+
font-size: 1rem;
|
309 |
+
font-weight: 600;
|
310 |
+
cursor: pointer;
|
311 |
+
transition: background 0.2s ease;
|
312 |
+
}
|
313 |
+
|
314 |
+
.continue-btn:hover {
|
315 |
+
background: #0056b3;
|
316 |
+
}
|
317 |
+
</style>
|
src/lib/components/Battle/BattleField.svelte
ADDED
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { onMount } from 'svelte';
|
3 |
+
import { fade } from 'svelte/transition';
|
4 |
+
import type { PicletInstance } from '$lib/db/schema';
|
5 |
+
import PicletInfo from './PicletInfo.svelte';
|
6 |
+
|
7 |
+
export let playerPiclet: PicletInstance;
|
8 |
+
export let enemyPiclet: PicletInstance;
|
9 |
+
export let playerHpPercentage: number;
|
10 |
+
export let enemyHpPercentage: number;
|
11 |
+
export let showIntro: boolean = false;
|
12 |
+
|
13 |
+
// Random background selection
|
14 |
+
const backgrounds = ['ruins', 'road', 'dinner', 'cave'];
|
15 |
+
const selectedBackground = backgrounds[Math.floor(Math.random() * backgrounds.length)];
|
16 |
+
|
17 |
+
// Animation states
|
18 |
+
let playerVisible = false;
|
19 |
+
let enemyVisible = false;
|
20 |
+
let trainerVisible = true;
|
21 |
+
|
22 |
+
onMount(() => {
|
23 |
+
if (showIntro) {
|
24 |
+
// Intro animation sequence
|
25 |
+
setTimeout(() => {
|
26 |
+
trainerVisible = false;
|
27 |
+
enemyVisible = true;
|
28 |
+
}, 500);
|
29 |
+
|
30 |
+
setTimeout(() => {
|
31 |
+
playerVisible = true;
|
32 |
+
}, 1500);
|
33 |
+
} else {
|
34 |
+
// Skip intro
|
35 |
+
playerVisible = true;
|
36 |
+
enemyVisible = true;
|
37 |
+
trainerVisible = false;
|
38 |
+
}
|
39 |
+
});
|
40 |
+
</script>
|
41 |
+
|
42 |
+
<div class="battle-field" style="background-image: url('/images/environments/{selectedBackground}.png')">
|
43 |
+
<!-- Striped overlay pattern -->
|
44 |
+
<div class="stripe-pattern"></div>
|
45 |
+
|
46 |
+
<!-- Trainer intro image -->
|
47 |
+
{#if showIntro && trainerVisible}
|
48 |
+
<div class="trainer-intro" transition:fade={{ duration: 300 }}>
|
49 |
+
<img src="/images/default_trainer.png" alt="Trainer" />
|
50 |
+
</div>
|
51 |
+
{/if}
|
52 |
+
|
53 |
+
<!-- Enemy area -->
|
54 |
+
<div class="enemy-area">
|
55 |
+
<PicletInfo
|
56 |
+
piclet={enemyPiclet}
|
57 |
+
hpPercentage={enemyHpPercentage}
|
58 |
+
isPlayer={false}
|
59 |
+
/>
|
60 |
+
|
61 |
+
{#if enemyVisible}
|
62 |
+
<div class="piclet-sprite enemy-sprite" transition:fade={{ duration: 300 }}>
|
63 |
+
<img
|
64 |
+
src={enemyPiclet.imageUrl}
|
65 |
+
alt={enemyPiclet.nickname}
|
66 |
+
on:error={(e) => e.currentTarget.src = 'https://via.placeholder.com/120x120?text=Piclet'}
|
67 |
+
/>
|
68 |
+
</div>
|
69 |
+
{/if}
|
70 |
+
</div>
|
71 |
+
|
72 |
+
<!-- Player area -->
|
73 |
+
<div class="player-area">
|
74 |
+
{#if playerVisible}
|
75 |
+
<div class="piclet-sprite player-sprite" transition:fade={{ duration: 300 }}>
|
76 |
+
<img
|
77 |
+
src={playerPiclet.imageUrl}
|
78 |
+
alt={playerPiclet.nickname}
|
79 |
+
on:error={(e) => e.currentTarget.src = 'https://via.placeholder.com/120x120?text=Piclet'}
|
80 |
+
/>
|
81 |
+
</div>
|
82 |
+
{/if}
|
83 |
+
|
84 |
+
<PicletInfo
|
85 |
+
piclet={playerPiclet}
|
86 |
+
hpPercentage={playerHpPercentage}
|
87 |
+
isPlayer={true}
|
88 |
+
/>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
|
92 |
+
<style>
|
93 |
+
.battle-field {
|
94 |
+
height: 60vh;
|
95 |
+
min-height: 400px;
|
96 |
+
position: relative;
|
97 |
+
background-size: cover;
|
98 |
+
background-position: center;
|
99 |
+
background-repeat: no-repeat;
|
100 |
+
overflow: hidden;
|
101 |
+
}
|
102 |
+
|
103 |
+
.stripe-pattern {
|
104 |
+
position: absolute;
|
105 |
+
inset: 0;
|
106 |
+
background: repeating-linear-gradient(
|
107 |
+
45deg,
|
108 |
+
transparent,
|
109 |
+
transparent 10px,
|
110 |
+
rgba(255, 255, 255, 0.05) 10px,
|
111 |
+
rgba(255, 255, 255, 0.05) 20px
|
112 |
+
);
|
113 |
+
pointer-events: none;
|
114 |
+
}
|
115 |
+
|
116 |
+
.trainer-intro {
|
117 |
+
position: absolute;
|
118 |
+
top: 50%;
|
119 |
+
left: 50%;
|
120 |
+
transform: translate(-50%, -50%);
|
121 |
+
z-index: 10;
|
122 |
+
}
|
123 |
+
|
124 |
+
.trainer-intro img {
|
125 |
+
width: 200px;
|
126 |
+
height: auto;
|
127 |
+
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
128 |
+
}
|
129 |
+
|
130 |
+
.enemy-area {
|
131 |
+
position: absolute;
|
132 |
+
top: 10%;
|
133 |
+
right: 10%;
|
134 |
+
display: flex;
|
135 |
+
flex-direction: column;
|
136 |
+
align-items: flex-end;
|
137 |
+
gap: 1rem;
|
138 |
+
}
|
139 |
+
|
140 |
+
.player-area {
|
141 |
+
position: absolute;
|
142 |
+
bottom: 10%;
|
143 |
+
left: 10%;
|
144 |
+
display: flex;
|
145 |
+
flex-direction: column;
|
146 |
+
align-items: flex-start;
|
147 |
+
gap: 1rem;
|
148 |
+
}
|
149 |
+
|
150 |
+
.piclet-sprite {
|
151 |
+
width: 120px;
|
152 |
+
height: 120px;
|
153 |
+
display: flex;
|
154 |
+
align-items: center;
|
155 |
+
justify-content: center;
|
156 |
+
}
|
157 |
+
|
158 |
+
.piclet-sprite img {
|
159 |
+
width: 100%;
|
160 |
+
height: 100%;
|
161 |
+
object-fit: contain;
|
162 |
+
image-rendering: pixelated;
|
163 |
+
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
164 |
+
}
|
165 |
+
|
166 |
+
.enemy-sprite {
|
167 |
+
order: 2;
|
168 |
+
}
|
169 |
+
|
170 |
+
.player-sprite {
|
171 |
+
order: 1;
|
172 |
+
transform: scaleX(-1); /* Mirror player sprite */
|
173 |
+
}
|
174 |
+
|
175 |
+
@media (max-width: 768px) {
|
176 |
+
.battle-field {
|
177 |
+
height: 50vh;
|
178 |
+
min-height: 300px;
|
179 |
+
}
|
180 |
+
|
181 |
+
.piclet-sprite {
|
182 |
+
width: 80px;
|
183 |
+
height: 80px;
|
184 |
+
}
|
185 |
+
|
186 |
+
.enemy-area {
|
187 |
+
top: 5%;
|
188 |
+
right: 5%;
|
189 |
+
}
|
190 |
+
|
191 |
+
.player-area {
|
192 |
+
bottom: 5%;
|
193 |
+
left: 5%;
|
194 |
+
}
|
195 |
+
}
|
196 |
+
</style>
|
src/lib/components/Battle/PicletInfo.svelte
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { PicletInstance } from '$lib/db/schema';
|
3 |
+
|
4 |
+
export let piclet: PicletInstance;
|
5 |
+
export let hpPercentage: number;
|
6 |
+
export let isPlayer: boolean;
|
7 |
+
|
8 |
+
$: hpColor = hpPercentage > 0.5 ? '#4caf50' : hpPercentage > 0.25 ? '#ffc107' : '#f44336';
|
9 |
+
$: displayHp = Math.max(0, Math.round(piclet.currentHp * hpPercentage));
|
10 |
+
</script>
|
11 |
+
|
12 |
+
<div class="piclet-info {isPlayer ? 'player-info' : 'enemy-info'}">
|
13 |
+
<div class="info-header">
|
14 |
+
<span class="piclet-name">{piclet.nickname}</span>
|
15 |
+
<span class="piclet-level">Lv.{piclet.level}</span>
|
16 |
+
</div>
|
17 |
+
|
18 |
+
<div class="hp-container">
|
19 |
+
<div class="hp-label">HP</div>
|
20 |
+
<div class="hp-bar-wrapper">
|
21 |
+
<div class="hp-bar-bg">
|
22 |
+
<div
|
23 |
+
class="hp-bar-fill"
|
24 |
+
style="width: {hpPercentage * 100}%; background-color: {hpColor}"
|
25 |
+
></div>
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
</div>
|
29 |
+
|
30 |
+
{#if isPlayer}
|
31 |
+
<div class="hp-text">
|
32 |
+
{displayHp} / {piclet.maxHp}
|
33 |
+
</div>
|
34 |
+
{/if}
|
35 |
+
</div>
|
36 |
+
|
37 |
+
<style>
|
38 |
+
.piclet-info {
|
39 |
+
background: rgba(255, 255, 255, 0.95);
|
40 |
+
border: 2px solid #333;
|
41 |
+
border-radius: 8px;
|
42 |
+
padding: 0.75rem 1rem;
|
43 |
+
min-width: 200px;
|
44 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
45 |
+
}
|
46 |
+
|
47 |
+
.player-info {
|
48 |
+
order: 2;
|
49 |
+
}
|
50 |
+
|
51 |
+
.enemy-info {
|
52 |
+
order: 1;
|
53 |
+
}
|
54 |
+
|
55 |
+
.info-header {
|
56 |
+
display: flex;
|
57 |
+
justify-content: space-between;
|
58 |
+
align-items: center;
|
59 |
+
margin-bottom: 0.5rem;
|
60 |
+
}
|
61 |
+
|
62 |
+
.piclet-name {
|
63 |
+
font-weight: 600;
|
64 |
+
font-size: 1rem;
|
65 |
+
color: #1a1a1a;
|
66 |
+
}
|
67 |
+
|
68 |
+
.piclet-level {
|
69 |
+
font-size: 0.875rem;
|
70 |
+
color: #666;
|
71 |
+
}
|
72 |
+
|
73 |
+
.hp-container {
|
74 |
+
display: flex;
|
75 |
+
align-items: center;
|
76 |
+
gap: 0.5rem;
|
77 |
+
}
|
78 |
+
|
79 |
+
.hp-label {
|
80 |
+
font-weight: 600;
|
81 |
+
font-size: 0.75rem;
|
82 |
+
color: #666;
|
83 |
+
}
|
84 |
+
|
85 |
+
.hp-bar-wrapper {
|
86 |
+
flex: 1;
|
87 |
+
}
|
88 |
+
|
89 |
+
.hp-bar-bg {
|
90 |
+
height: 8px;
|
91 |
+
background: #e0e0e0;
|
92 |
+
border-radius: 4px;
|
93 |
+
overflow: hidden;
|
94 |
+
position: relative;
|
95 |
+
}
|
96 |
+
|
97 |
+
.hp-bar-fill {
|
98 |
+
height: 100%;
|
99 |
+
transition: width 0.5s ease, background-color 0.3s ease;
|
100 |
+
border-radius: 4px;
|
101 |
+
}
|
102 |
+
|
103 |
+
.hp-text {
|
104 |
+
text-align: right;
|
105 |
+
font-size: 0.75rem;
|
106 |
+
color: #666;
|
107 |
+
margin-top: 0.25rem;
|
108 |
+
}
|
109 |
+
|
110 |
+
@media (max-width: 768px) {
|
111 |
+
.piclet-info {
|
112 |
+
min-width: 150px;
|
113 |
+
padding: 0.5rem 0.75rem;
|
114 |
+
}
|
115 |
+
|
116 |
+
.piclet-name {
|
117 |
+
font-size: 0.875rem;
|
118 |
+
}
|
119 |
+
|
120 |
+
.piclet-level {
|
121 |
+
font-size: 0.75rem;
|
122 |
+
}
|
123 |
+
}
|
124 |
+
</style>
|
src/lib/components/Pages/Battle.svelte
ADDED
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { onMount } from 'svelte';
|
3 |
+
import { fade } from 'svelte/transition';
|
4 |
+
import type { PicletInstance } from '$lib/db/schema';
|
5 |
+
import BattleField from '../Battle/BattleField.svelte';
|
6 |
+
import BattleControls from '../Battle/BattleControls.svelte';
|
7 |
+
|
8 |
+
export let playerPiclet: PicletInstance;
|
9 |
+
export let enemyPiclet: PicletInstance;
|
10 |
+
export let isWildBattle: boolean = true;
|
11 |
+
export let onBattleEnd: (result: any) => void = () => {};
|
12 |
+
|
13 |
+
// Battle state
|
14 |
+
let currentMessage = isWildBattle
|
15 |
+
? `A wild ${enemyPiclet.nickname} appeared!`
|
16 |
+
: `Trainer wants to battle!`;
|
17 |
+
let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended' = 'intro';
|
18 |
+
let processingTurn = false;
|
19 |
+
let battleEnded = false;
|
20 |
+
|
21 |
+
// HP animation states
|
22 |
+
let playerHpPercentage = playerPiclet.currentHp / playerPiclet.maxHp;
|
23 |
+
let enemyHpPercentage = enemyPiclet.currentHp / enemyPiclet.maxHp;
|
24 |
+
|
25 |
+
onMount(() => {
|
26 |
+
// Start intro sequence
|
27 |
+
setTimeout(() => {
|
28 |
+
currentMessage = `Go, ${playerPiclet.nickname}!`;
|
29 |
+
setTimeout(() => {
|
30 |
+
currentMessage = `What will ${playerPiclet.nickname} do?`;
|
31 |
+
battlePhase = 'main';
|
32 |
+
}, 1500);
|
33 |
+
}, 2000);
|
34 |
+
});
|
35 |
+
|
36 |
+
function handleAction(action: string) {
|
37 |
+
if (processingTurn || battleEnded) return;
|
38 |
+
|
39 |
+
switch (action) {
|
40 |
+
case 'fight':
|
41 |
+
battlePhase = 'moveSelect';
|
42 |
+
break;
|
43 |
+
case 'piclet':
|
44 |
+
battlePhase = 'picletSelect';
|
45 |
+
break;
|
46 |
+
case 'catch':
|
47 |
+
if (isWildBattle) {
|
48 |
+
processingTurn = true;
|
49 |
+
currentMessage = 'You threw a Piclet Ball!';
|
50 |
+
setTimeout(() => {
|
51 |
+
currentMessage = 'The wild piclet broke free!';
|
52 |
+
processingTurn = false;
|
53 |
+
}, 2000);
|
54 |
+
}
|
55 |
+
break;
|
56 |
+
case 'run':
|
57 |
+
if (isWildBattle) {
|
58 |
+
currentMessage = 'Got away safely!';
|
59 |
+
battleEnded = true;
|
60 |
+
setTimeout(() => onBattleEnd(false), 1500);
|
61 |
+
} else {
|
62 |
+
currentMessage = "You can't run from a trainer battle!";
|
63 |
+
}
|
64 |
+
break;
|
65 |
+
}
|
66 |
+
}
|
67 |
+
|
68 |
+
function handleMoveSelect(move: any) {
|
69 |
+
battlePhase = 'main';
|
70 |
+
processingTurn = true;
|
71 |
+
currentMessage = `${playerPiclet.nickname} used ${move.name}!`;
|
72 |
+
|
73 |
+
// Mock damage
|
74 |
+
setTimeout(() => {
|
75 |
+
enemyHpPercentage = Math.max(0, enemyHpPercentage - 0.2);
|
76 |
+
|
77 |
+
if (enemyHpPercentage <= 0) {
|
78 |
+
currentMessage = `${enemyPiclet.nickname} fainted!`;
|
79 |
+
battleEnded = true;
|
80 |
+
setTimeout(() => onBattleEnd(true), 2000);
|
81 |
+
} else {
|
82 |
+
// Enemy turn
|
83 |
+
setTimeout(() => {
|
84 |
+
const enemyMove = enemyPiclet.moves[0];
|
85 |
+
currentMessage = `${enemyPiclet.nickname} used ${enemyMove.name}!`;
|
86 |
+
|
87 |
+
setTimeout(() => {
|
88 |
+
playerHpPercentage = Math.max(0, playerHpPercentage - 0.15);
|
89 |
+
|
90 |
+
if (playerHpPercentage <= 0) {
|
91 |
+
currentMessage = `${playerPiclet.nickname} fainted!`;
|
92 |
+
battleEnded = true;
|
93 |
+
setTimeout(() => onBattleEnd(false), 2000);
|
94 |
+
} else {
|
95 |
+
currentMessage = `What will ${playerPiclet.nickname} do?`;
|
96 |
+
processingTurn = false;
|
97 |
+
}
|
98 |
+
}, 1500);
|
99 |
+
}, 1500);
|
100 |
+
}
|
101 |
+
}, 1500);
|
102 |
+
}
|
103 |
+
|
104 |
+
function handlePicletSelect(piclet: PicletInstance) {
|
105 |
+
battlePhase = 'main';
|
106 |
+
currentMessage = `Come back, ${playerPiclet.nickname}!`;
|
107 |
+
setTimeout(() => {
|
108 |
+
playerPiclet = piclet;
|
109 |
+
playerHpPercentage = piclet.currentHp / piclet.maxHp;
|
110 |
+
currentMessage = `Go, ${piclet.nickname}!`;
|
111 |
+
setTimeout(() => {
|
112 |
+
currentMessage = `What will ${piclet.nickname} do?`;
|
113 |
+
}, 1500);
|
114 |
+
}, 1500);
|
115 |
+
}
|
116 |
+
|
117 |
+
function handleBack() {
|
118 |
+
battlePhase = 'main';
|
119 |
+
}
|
120 |
+
</script>
|
121 |
+
|
122 |
+
<div class="battle-page">
|
123 |
+
<nav class="battle-nav">
|
124 |
+
<button class="back-button" on:click={() => onBattleEnd('cancelled')}>
|
125 |
+
← Back
|
126 |
+
</button>
|
127 |
+
<h1>{isWildBattle ? 'Wild Battle' : 'Battle'}</h1>
|
128 |
+
<div class="nav-spacer"></div>
|
129 |
+
</nav>
|
130 |
+
|
131 |
+
<div class="battle-content">
|
132 |
+
<BattleField
|
133 |
+
{playerPiclet}
|
134 |
+
{enemyPiclet}
|
135 |
+
{playerHpPercentage}
|
136 |
+
{enemyHpPercentage}
|
137 |
+
showIntro={battlePhase === 'intro'}
|
138 |
+
/>
|
139 |
+
|
140 |
+
<BattleControls
|
141 |
+
{currentMessage}
|
142 |
+
{battlePhase}
|
143 |
+
{processingTurn}
|
144 |
+
{battleEnded}
|
145 |
+
{isWildBattle}
|
146 |
+
{playerPiclet}
|
147 |
+
{enemyPiclet}
|
148 |
+
onAction={handleAction}
|
149 |
+
onMoveSelect={handleMoveSelect}
|
150 |
+
onPicletSelect={handlePicletSelect}
|
151 |
+
onBack={handleBack}
|
152 |
+
/>
|
153 |
+
</div>
|
154 |
+
</div>
|
155 |
+
|
156 |
+
<style>
|
157 |
+
.battle-page {
|
158 |
+
height: 100vh;
|
159 |
+
display: flex;
|
160 |
+
flex-direction: column;
|
161 |
+
background: #f8f9fa;
|
162 |
+
overflow: hidden;
|
163 |
+
}
|
164 |
+
|
165 |
+
.battle-nav {
|
166 |
+
display: flex;
|
167 |
+
align-items: center;
|
168 |
+
justify-content: space-between;
|
169 |
+
padding: 1rem;
|
170 |
+
background: white;
|
171 |
+
border-bottom: 1px solid #e0e0e0;
|
172 |
+
position: relative;
|
173 |
+
z-index: 10;
|
174 |
+
}
|
175 |
+
|
176 |
+
.back-button {
|
177 |
+
background: none;
|
178 |
+
border: none;
|
179 |
+
color: #007bff;
|
180 |
+
font-size: 1rem;
|
181 |
+
cursor: pointer;
|
182 |
+
padding: 0.5rem;
|
183 |
+
}
|
184 |
+
|
185 |
+
.battle-nav h1 {
|
186 |
+
margin: 0;
|
187 |
+
font-size: 1.25rem;
|
188 |
+
font-weight: 600;
|
189 |
+
color: #1a1a1a;
|
190 |
+
position: absolute;
|
191 |
+
left: 50%;
|
192 |
+
transform: translateX(-50%);
|
193 |
+
}
|
194 |
+
|
195 |
+
.nav-spacer {
|
196 |
+
width: 60px;
|
197 |
+
}
|
198 |
+
|
199 |
+
.battle-content {
|
200 |
+
flex: 1;
|
201 |
+
display: flex;
|
202 |
+
flex-direction: column;
|
203 |
+
overflow: hidden;
|
204 |
+
}
|
205 |
+
</style>
|
src/lib/components/Pages/Encounters.svelte
CHANGED
@@ -1,17 +1,24 @@
|
|
1 |
<script lang="ts">
|
2 |
import { onMount } from 'svelte';
|
3 |
import { fade, fly } from 'svelte/transition';
|
4 |
-
import type { Encounter, GameState } from '$lib/db/schema';
|
5 |
import { EncounterType } from '$lib/db/schema';
|
6 |
import { EncounterService } from '$lib/db/encounterService';
|
7 |
import { getOrCreateGameState, incrementCounter, addProgressPoints } from '$lib/db/gameState';
|
8 |
import { db } from '$lib/db';
|
|
|
9 |
|
10 |
let encounters: Encounter[] = [];
|
11 |
let gameState: GameState | null = null;
|
12 |
let isLoading = true;
|
13 |
let isRefreshing = false;
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
onMount(async () => {
|
16 |
await loadEncounters();
|
17 |
});
|
@@ -61,8 +68,8 @@
|
|
61 |
}
|
62 |
isLoading = false;
|
63 |
} else {
|
64 |
-
// Regular wild encounter -
|
65 |
-
|
66 |
}
|
67 |
} else if (encounter.type === EncounterType.SHOP) {
|
68 |
await handleShopEncounter();
|
@@ -135,8 +142,121 @@
|
|
135 |
return '#607d8b';
|
136 |
}
|
137 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
</script>
|
139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
140 |
<div class="encounters-page">
|
141 |
<div class="header">
|
142 |
<h1>Encounters</h1>
|
@@ -155,11 +275,11 @@
|
|
155 |
</div>
|
156 |
{:else if encounters.length === 0}
|
157 |
<div class="empty-state">
|
158 |
-
<div class="empty-icon"
|
159 |
-
<h2>No
|
160 |
-
<p>
|
161 |
-
<button class="
|
162 |
-
|
163 |
</button>
|
164 |
</div>
|
165 |
{:else}
|
@@ -175,7 +295,18 @@
|
|
175 |
<div class="encounter-icon">
|
176 |
{#if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
|
177 |
{#if encounter.title === 'Your First Piclet!'}
|
178 |
-
<div class="piclet-silhouette"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
{:else}
|
180 |
<img
|
181 |
src={`https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`}
|
@@ -203,6 +334,7 @@
|
|
203 |
</div>
|
204 |
{/if}
|
205 |
</div>
|
|
|
206 |
|
207 |
<style>
|
208 |
.encounters-page {
|
@@ -337,14 +469,32 @@
|
|
337 |
.piclet-silhouette {
|
338 |
width: 100%;
|
339 |
height: 100%;
|
340 |
-
background: #e0e0e0;
|
341 |
border-radius: 8px;
|
342 |
display: flex;
|
343 |
align-items: center;
|
344 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
font-size: 2rem;
|
346 |
font-weight: bold;
|
347 |
color: #999;
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
}
|
349 |
|
350 |
.type-icon, .fallback-icon {
|
@@ -373,10 +523,10 @@
|
|
373 |
color: #999;
|
374 |
}
|
375 |
|
376 |
-
.
|
377 |
margin-top: 1rem;
|
378 |
padding: 0.75rem 1.5rem;
|
379 |
-
background: #
|
380 |
color: white;
|
381 |
border: none;
|
382 |
border-radius: 8px;
|
@@ -386,7 +536,7 @@
|
|
386 |
transition: background 0.2s ease;
|
387 |
}
|
388 |
|
389 |
-
.
|
390 |
-
background: #
|
391 |
}
|
392 |
</style>
|
|
|
1 |
<script lang="ts">
|
2 |
import { onMount } from 'svelte';
|
3 |
import { fade, fly } from 'svelte/transition';
|
4 |
+
import type { Encounter, GameState, PicletInstance } from '$lib/db/schema';
|
5 |
import { EncounterType } from '$lib/db/schema';
|
6 |
import { EncounterService } from '$lib/db/encounterService';
|
7 |
import { getOrCreateGameState, incrementCounter, addProgressPoints } from '$lib/db/gameState';
|
8 |
import { db } from '$lib/db';
|
9 |
+
import Battle from './Battle.svelte';
|
10 |
|
11 |
let encounters: Encounter[] = [];
|
12 |
let gameState: GameState | null = null;
|
13 |
let isLoading = true;
|
14 |
let isRefreshing = false;
|
15 |
|
16 |
+
// Battle state
|
17 |
+
let showBattle = false;
|
18 |
+
let battlePlayerPiclet: PicletInstance | null = null;
|
19 |
+
let battleEnemyPiclet: PicletInstance | null = null;
|
20 |
+
let battleIsWild = true;
|
21 |
+
|
22 |
onMount(async () => {
|
23 |
await loadEncounters();
|
24 |
});
|
|
|
68 |
}
|
69 |
isLoading = false;
|
70 |
} else {
|
71 |
+
// Regular wild encounter - start battle
|
72 |
+
await startBattle(encounter);
|
73 |
}
|
74 |
} else if (encounter.type === EncounterType.SHOP) {
|
75 |
await handleShopEncounter();
|
|
|
142 |
return '#607d8b';
|
143 |
}
|
144 |
}
|
145 |
+
|
146 |
+
async function startBattle(encounter: Encounter) {
|
147 |
+
try {
|
148 |
+
// Get player's first healthy piclet
|
149 |
+
const roster = await db.picletInstances
|
150 |
+
.where('isInRoster')
|
151 |
+
.equals(1)
|
152 |
+
.toArray();
|
153 |
+
|
154 |
+
const healthyPiclets = roster.filter(p => p.currentHp > 0);
|
155 |
+
|
156 |
+
if (healthyPiclets.length === 0) {
|
157 |
+
alert('You need at least one healthy piclet in your roster to battle!');
|
158 |
+
return;
|
159 |
+
}
|
160 |
+
|
161 |
+
// Generate enemy piclet for battle
|
162 |
+
const enemyPiclet = await generateEnemyPiclet(encounter);
|
163 |
+
if (!enemyPiclet) return;
|
164 |
+
|
165 |
+
// Set up battle
|
166 |
+
battlePlayerPiclet = healthyPiclets[0];
|
167 |
+
battleEnemyPiclet = enemyPiclet;
|
168 |
+
battleIsWild = true;
|
169 |
+
showBattle = true;
|
170 |
+
} catch (error) {
|
171 |
+
console.error('Error starting battle:', error);
|
172 |
+
}
|
173 |
+
}
|
174 |
+
|
175 |
+
async function generateEnemyPiclet(encounter: Encounter): Promise<PicletInstance | null> {
|
176 |
+
if (!encounter.picletTypeId || !encounter.enemyLevel) return null;
|
177 |
+
|
178 |
+
// Create a mock enemy piclet instance
|
179 |
+
const enemyPiclet: PicletInstance = {
|
180 |
+
id: -1, // Temporary ID for enemy
|
181 |
+
typeId: encounter.picletTypeId,
|
182 |
+
nickname: `Wild Piclet`,
|
183 |
+
primaryTypeString: 'normal',
|
184 |
+
|
185 |
+
level: encounter.enemyLevel,
|
186 |
+
xp: 0,
|
187 |
+
currentHp: 20 + (encounter.enemyLevel * 5),
|
188 |
+
maxHp: 20 + (encounter.enemyLevel * 5),
|
189 |
+
attack: 10 + (encounter.enemyLevel * 2),
|
190 |
+
defense: 10 + (encounter.enemyLevel * 2),
|
191 |
+
fieldAttack: 10 + (encounter.enemyLevel * 2),
|
192 |
+
fieldDefense: 10 + (encounter.enemyLevel * 2),
|
193 |
+
speed: 10 + (encounter.enemyLevel * 2),
|
194 |
+
|
195 |
+
baseHp: 20,
|
196 |
+
baseAttack: 10,
|
197 |
+
baseDefense: 10,
|
198 |
+
baseFieldAttack: 10,
|
199 |
+
baseFieldDefense: 10,
|
200 |
+
baseSpeed: 10,
|
201 |
+
|
202 |
+
moves: [
|
203 |
+
{
|
204 |
+
name: 'Tackle',
|
205 |
+
type: 'normal',
|
206 |
+
power: 40,
|
207 |
+
accuracy: 100,
|
208 |
+
pp: 35,
|
209 |
+
currentPp: 35,
|
210 |
+
description: 'A physical attack'
|
211 |
+
}
|
212 |
+
],
|
213 |
+
nature: 'hardy',
|
214 |
+
|
215 |
+
isInRoster: false,
|
216 |
+
caughtAt: new Date(),
|
217 |
+
bst: 60,
|
218 |
+
tier: 'common',
|
219 |
+
role: 'balanced',
|
220 |
+
variance: 1,
|
221 |
+
|
222 |
+
imageUrl: `https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`,
|
223 |
+
imageCaption: 'A wild piclet',
|
224 |
+
concept: 'wild',
|
225 |
+
imagePrompt: 'wild piclet'
|
226 |
+
};
|
227 |
+
|
228 |
+
return enemyPiclet;
|
229 |
+
}
|
230 |
+
|
231 |
+
function handleBattleEnd(result: any) {
|
232 |
+
showBattle = false;
|
233 |
+
|
234 |
+
if (result === true) {
|
235 |
+
// Victory
|
236 |
+
console.log('Battle won!');
|
237 |
+
} else if (result === false) {
|
238 |
+
// Defeat or ran away
|
239 |
+
console.log('Battle lost or fled');
|
240 |
+
} else if (result && result.id) {
|
241 |
+
// Caught a piclet
|
242 |
+
console.log('Piclet caught!', result);
|
243 |
+
incrementCounter('picletsCapured');
|
244 |
+
addProgressPoints(100);
|
245 |
+
}
|
246 |
+
|
247 |
+
// Force refresh encounters after battle
|
248 |
+
forceEncounterRefresh();
|
249 |
+
}
|
250 |
</script>
|
251 |
|
252 |
+
{#if showBattle && battlePlayerPiclet && battleEnemyPiclet}
|
253 |
+
<Battle
|
254 |
+
playerPiclet={battlePlayerPiclet}
|
255 |
+
enemyPiclet={battleEnemyPiclet}
|
256 |
+
isWildBattle={battleIsWild}
|
257 |
+
onBattleEnd={handleBattleEnd}
|
258 |
+
/>
|
259 |
+
{:else}
|
260 |
<div class="encounters-page">
|
261 |
<div class="header">
|
262 |
<h1>Encounters</h1>
|
|
|
275 |
</div>
|
276 |
{:else if encounters.length === 0}
|
277 |
<div class="empty-state">
|
278 |
+
<div class="empty-icon">📸</div>
|
279 |
+
<h2>No Piclets Discovered</h2>
|
280 |
+
<p>Scan your first Piclet to start your adventure!</p>
|
281 |
+
<button class="scan-button" on:click={() => window.location.hash = '#/pictuary'}>
|
282 |
+
Go to Scanner
|
283 |
</button>
|
284 |
</div>
|
285 |
{:else}
|
|
|
295 |
<div class="encounter-icon">
|
296 |
{#if encounter.type === EncounterType.WILD_PICLET && encounter.picletTypeId}
|
297 |
{#if encounter.title === 'Your First Piclet!'}
|
298 |
+
<div class="piclet-silhouette">
|
299 |
+
<img
|
300 |
+
src={`https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`}
|
301 |
+
alt="Mystery Piclet"
|
302 |
+
class="silhouette-img"
|
303 |
+
on:error={(e) => {
|
304 |
+
e.currentTarget.style.display = 'none';
|
305 |
+
e.currentTarget.nextElementSibling.style.display = 'block';
|
306 |
+
}}
|
307 |
+
/>
|
308 |
+
<div class="silhouette-fallback" style="display: none">?</div>
|
309 |
+
</div>
|
310 |
{:else}
|
311 |
<img
|
312 |
src={`https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`}
|
|
|
334 |
</div>
|
335 |
{/if}
|
336 |
</div>
|
337 |
+
{/if}
|
338 |
|
339 |
<style>
|
340 |
.encounters-page {
|
|
|
469 |
.piclet-silhouette {
|
470 |
width: 100%;
|
471 |
height: 100%;
|
|
|
472 |
border-radius: 8px;
|
473 |
display: flex;
|
474 |
align-items: center;
|
475 |
justify-content: center;
|
476 |
+
position: relative;
|
477 |
+
overflow: hidden;
|
478 |
+
}
|
479 |
+
|
480 |
+
.silhouette-img {
|
481 |
+
width: 100%;
|
482 |
+
height: 100%;
|
483 |
+
object-fit: cover;
|
484 |
+
filter: grayscale(100%) brightness(0.3) contrast(0.5);
|
485 |
+
opacity: 0.8;
|
486 |
+
}
|
487 |
+
|
488 |
+
.silhouette-fallback {
|
489 |
font-size: 2rem;
|
490 |
font-weight: bold;
|
491 |
color: #999;
|
492 |
+
background: #e0e0e0;
|
493 |
+
width: 100%;
|
494 |
+
height: 100%;
|
495 |
+
display: flex;
|
496 |
+
align-items: center;
|
497 |
+
justify-content: center;
|
498 |
}
|
499 |
|
500 |
.type-icon, .fallback-icon {
|
|
|
523 |
color: #999;
|
524 |
}
|
525 |
|
526 |
+
.scan-button {
|
527 |
margin-top: 1rem;
|
528 |
padding: 0.75rem 1.5rem;
|
529 |
+
background: #007bff;
|
530 |
color: white;
|
531 |
border: none;
|
532 |
border-radius: 8px;
|
|
|
536 |
transition: background 0.2s ease;
|
537 |
}
|
538 |
|
539 |
+
.scan-button:hover {
|
540 |
+
background: #0056b3;
|
541 |
}
|
542 |
</style>
|
src/lib/components/Piclets/AddToRosterDialog.svelte
CHANGED
@@ -29,7 +29,7 @@
|
|
29 |
}
|
30 |
</script>
|
31 |
|
32 |
-
<div class="dialog-overlay" onclick={(e) => e.target === e.currentTarget && onClose()}>
|
33 |
<div class="dialog-content">
|
34 |
<header class="dialog-header">
|
35 |
<h2>Add to Roster</h2>
|
|
|
29 |
}
|
30 |
</script>
|
31 |
|
32 |
+
<div class="dialog-overlay" onclick={(e) => e.target === e.currentTarget && onClose()} onkeydown={(e) => e.key === 'Escape' && onClose()} role="button" tabindex="0" aria-label="Close dialog">
|
33 |
<div class="dialog-content">
|
34 |
<header class="dialog-header">
|
35 |
<h2>Add to Roster</h2>
|
src/lib/components/Piclets/PicletDetail.svelte
CHANGED
@@ -57,7 +57,7 @@
|
|
57 |
|
58 |
<div class="detail-page">
|
59 |
<div class="navigation-bar">
|
60 |
-
<button class="back-btn" onclick={onClose}>
|
61 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
62 |
<path d="M19 12H5m0 0l7 7m-7-7l7-7"></path>
|
63 |
</svg>
|
|
|
57 |
|
58 |
<div class="detail-page">
|
59 |
<div class="navigation-bar">
|
60 |
+
<button class="back-btn" onclick={onClose} aria-label="Go back">
|
61 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
62 |
<path d="M19 12H5m0 0l7 7m-7-7l7-7"></path>
|
63 |
</svg>
|
src/lib/db/battleService.ts
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { PicletInstance, BattleState, BattlePhase } from './schema';
|
2 |
+
import { db } from './index';
|
3 |
+
|
4 |
+
export class BattleService {
|
5 |
+
// Initialize a new battle
|
6 |
+
static createBattleState(
|
7 |
+
playerPiclet: PicletInstance,
|
8 |
+
enemyPiclet: PicletInstance,
|
9 |
+
isWildBattle: boolean = true
|
10 |
+
): BattleState {
|
11 |
+
return {
|
12 |
+
phase: 'intro' as BattlePhase,
|
13 |
+
currentTurn: 0,
|
14 |
+
playerPiclet,
|
15 |
+
enemyPiclet,
|
16 |
+
isWildBattle,
|
17 |
+
processingTurn: false,
|
18 |
+
battleEnded: false
|
19 |
+
};
|
20 |
+
}
|
21 |
+
|
22 |
+
// Calculate damage (simplified formula)
|
23 |
+
static calculateDamage(
|
24 |
+
attacker: PicletInstance,
|
25 |
+
defender: PicletInstance,
|
26 |
+
move: any
|
27 |
+
): number {
|
28 |
+
const baseDamage = move.power || 50;
|
29 |
+
const attackStat = move.type === 'physical' ? attacker.attack : attacker.fieldAttack;
|
30 |
+
const defenseStat = move.type === 'physical' ? defender.defense : defender.fieldDefense;
|
31 |
+
|
32 |
+
// Simple damage formula
|
33 |
+
const damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10);
|
34 |
+
|
35 |
+
// Add some randomness (85-100% of calculated damage)
|
36 |
+
const randomFactor = 0.85 + Math.random() * 0.15;
|
37 |
+
|
38 |
+
return Math.floor(damage * randomFactor);
|
39 |
+
}
|
40 |
+
|
41 |
+
// Check if move hits (based on accuracy)
|
42 |
+
static doesMoveHit(accuracy: number): boolean {
|
43 |
+
return Math.random() * 100 < accuracy;
|
44 |
+
}
|
45 |
+
|
46 |
+
// Calculate capture rate for wild piclets
|
47 |
+
static calculateCaptureRate(
|
48 |
+
targetPiclet: PicletInstance,
|
49 |
+
targetMaxHp: number
|
50 |
+
): number {
|
51 |
+
const hpFactor = (targetMaxHp - targetPiclet.currentHp) / targetMaxHp;
|
52 |
+
const levelFactor = Math.max(0.5, 1 - (targetPiclet.level / 100));
|
53 |
+
|
54 |
+
// Base capture rate increases with damage and lower level
|
55 |
+
const baseRate = 0.3; // 30% base rate
|
56 |
+
const captureRate = baseRate + (hpFactor * 0.4) + (levelFactor * 0.3);
|
57 |
+
|
58 |
+
return Math.min(0.95, captureRate); // Cap at 95%
|
59 |
+
}
|
60 |
+
|
61 |
+
// Attempt to catch a wild piclet
|
62 |
+
static attemptCapture(
|
63 |
+
targetPiclet: PicletInstance
|
64 |
+
): { success: boolean; shakes: number } {
|
65 |
+
const captureRate = this.calculateCaptureRate(targetPiclet, targetPiclet.maxHp);
|
66 |
+
const roll = Math.random();
|
67 |
+
|
68 |
+
// Calculate shakes (0-3)
|
69 |
+
let shakes = 0;
|
70 |
+
if (roll < captureRate * 0.9) shakes = 1;
|
71 |
+
if (roll < captureRate * 0.7) shakes = 2;
|
72 |
+
if (roll < captureRate * 0.5) shakes = 3;
|
73 |
+
|
74 |
+
return {
|
75 |
+
success: roll < captureRate,
|
76 |
+
shakes
|
77 |
+
};
|
78 |
+
}
|
79 |
+
|
80 |
+
// Create a caught piclet instance
|
81 |
+
static async createCaughtPiclet(
|
82 |
+
wildPiclet: PicletInstance
|
83 |
+
): Promise<PicletInstance> {
|
84 |
+
const caughtPiclet: Omit<PicletInstance, 'id'> = {
|
85 |
+
...wildPiclet,
|
86 |
+
isInRoster: false, // Goes to storage initially
|
87 |
+
rosterPosition: undefined,
|
88 |
+
caughtAt: new Date()
|
89 |
+
};
|
90 |
+
|
91 |
+
const id = await db.picletInstances.add(caughtPiclet);
|
92 |
+
return { ...caughtPiclet, id };
|
93 |
+
}
|
94 |
+
|
95 |
+
// Calculate experience gain
|
96 |
+
static calculateExpGain(
|
97 |
+
defeatedPiclet: PicletInstance,
|
98 |
+
isWild: boolean
|
99 |
+
): number {
|
100 |
+
const baseExp = 50 + (defeatedPiclet.level * 10);
|
101 |
+
const wildModifier = isWild ? 1 : 1.5; // Trainer piclets give more exp
|
102 |
+
|
103 |
+
return Math.floor(baseExp * wildModifier);
|
104 |
+
}
|
105 |
+
|
106 |
+
// Check if piclet should level up
|
107 |
+
static checkLevelUp(
|
108 |
+
piclet: PicletInstance,
|
109 |
+
expGained: number
|
110 |
+
): { leveledUp: boolean; newLevel: number } {
|
111 |
+
const newExp = piclet.xp + expGained;
|
112 |
+
const expForNextLevel = this.getExpForLevel(piclet.level + 1);
|
113 |
+
|
114 |
+
if (newExp >= expForNextLevel) {
|
115 |
+
return {
|
116 |
+
leveledUp: true,
|
117 |
+
newLevel: piclet.level + 1
|
118 |
+
};
|
119 |
+
}
|
120 |
+
|
121 |
+
return {
|
122 |
+
leveledUp: false,
|
123 |
+
newLevel: piclet.level
|
124 |
+
};
|
125 |
+
}
|
126 |
+
|
127 |
+
// Get experience required for a level
|
128 |
+
static getExpForLevel(level: number): number {
|
129 |
+
// Simple exponential growth formula
|
130 |
+
return Math.floor(Math.pow(level, 2.5) * 10);
|
131 |
+
}
|
132 |
+
|
133 |
+
// Apply stat boosts for level up
|
134 |
+
static applyLevelUpStats(piclet: PicletInstance): PicletInstance {
|
135 |
+
// Simple stat growth (5-10% increase per level)
|
136 |
+
const growthFactor = 1.07;
|
137 |
+
|
138 |
+
return {
|
139 |
+
...piclet,
|
140 |
+
level: piclet.level + 1,
|
141 |
+
maxHp: Math.floor(piclet.maxHp * growthFactor),
|
142 |
+
currentHp: Math.floor(piclet.currentHp * growthFactor),
|
143 |
+
attack: Math.floor(piclet.attack * growthFactor),
|
144 |
+
defense: Math.floor(piclet.defense * growthFactor),
|
145 |
+
fieldAttack: Math.floor(piclet.fieldAttack * growthFactor),
|
146 |
+
fieldDefense: Math.floor(piclet.fieldDefense * growthFactor),
|
147 |
+
speed: Math.floor(piclet.speed * growthFactor)
|
148 |
+
};
|
149 |
+
}
|
150 |
+
}
|
src/lib/db/encounterService.ts
CHANGED
@@ -40,17 +40,43 @@ export class EncounterService {
|
|
40 |
static async generateEncounters(): Promise<Encounter[]> {
|
41 |
const encounters: Omit<Encounter, 'id'>[] = [];
|
42 |
|
43 |
-
// Check if player
|
44 |
const playerPiclets = await db.picletInstances.toArray();
|
|
|
45 |
if (playerPiclets.length === 0) {
|
46 |
-
|
47 |
-
|
48 |
-
//
|
49 |
-
const
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
}
|
52 |
-
|
53 |
-
//
|
|
|
|
|
54 |
encounters.push({
|
55 |
type: EncounterType.SHOP,
|
56 |
title: 'Piclet Shop',
|
@@ -64,6 +90,10 @@ export class EncounterService {
|
|
64 |
description: 'Heal your piclets back to full health',
|
65 |
createdAt: new Date()
|
66 |
});
|
|
|
|
|
|
|
|
|
67 |
|
68 |
// Clear existing encounters and add new ones
|
69 |
await db.encounters.clear();
|
@@ -138,23 +168,25 @@ export class EncounterService {
|
|
138 |
|
139 |
// Catch a wild piclet (for first encounter)
|
140 |
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
|
141 |
-
//
|
142 |
-
|
|
|
|
|
143 |
const newPiclet: Omit<PicletInstance, 'id'> = {
|
144 |
typeId: encounter.picletTypeId!,
|
145 |
-
nickname: 'Starter Piclet',
|
146 |
primaryTypeString: 'normal',
|
147 |
|
148 |
-
// Stats
|
149 |
level: encounter.enemyLevel || 5,
|
150 |
xp: 0,
|
151 |
-
currentHp: 20,
|
152 |
-
maxHp: 20,
|
153 |
-
attack: 10,
|
154 |
-
defense: 10,
|
155 |
-
fieldAttack: 10,
|
156 |
-
fieldDefense: 10,
|
157 |
-
speed: 10,
|
158 |
|
159 |
// Base stats
|
160 |
baseHp: 20,
|
@@ -164,8 +196,18 @@ export class EncounterService {
|
|
164 |
baseFieldDefense: 10,
|
165 |
baseSpeed: 10,
|
166 |
|
167 |
-
// Battle
|
168 |
-
moves: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
nature: 'hardy',
|
170 |
|
171 |
// Roster
|
@@ -179,11 +221,12 @@ export class EncounterService {
|
|
179 |
role: 'balanced',
|
180 |
variance: 1,
|
181 |
|
182 |
-
// Visuals
|
183 |
-
imageUrl: `https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`,
|
184 |
-
|
185 |
-
|
186 |
-
|
|
|
187 |
};
|
188 |
|
189 |
const id = await db.picletInstances.add(newPiclet);
|
|
|
40 |
static async generateEncounters(): Promise<Encounter[]> {
|
41 |
const encounters: Omit<Encounter, 'id'>[] = [];
|
42 |
|
43 |
+
// Check if player has caught any piclets first
|
44 |
const playerPiclets = await db.picletInstances.toArray();
|
45 |
+
|
46 |
if (playerPiclets.length === 0) {
|
47 |
+
// No piclets caught yet - check for discovered piclets
|
48 |
+
// For now, we'll check the monsters collection as a proxy for discovered piclets
|
49 |
+
// In a real app, this would check a remote database for piclets discovered by scanning
|
50 |
+
const discoveredPiclets = await db.monsters.toArray();
|
51 |
+
|
52 |
+
if (discoveredPiclets.length === 0) {
|
53 |
+
// No piclets discovered yet - return empty encounters
|
54 |
+
await db.encounters.clear();
|
55 |
+
await markEncountersRefreshed();
|
56 |
+
return [];
|
57 |
+
}
|
58 |
+
|
59 |
+
// Player has discovered but not caught any piclets - show ONLY first catch encounter
|
60 |
+
const firstDiscovered = discoveredPiclets[0];
|
61 |
+
encounters.push({
|
62 |
+
type: EncounterType.WILD_PICLET,
|
63 |
+
title: 'Your First Piclet!',
|
64 |
+
description: 'A friendly piclet appears! This one seems easy to catch.',
|
65 |
+
picletTypeId: firstDiscovered.id?.toString() || 'starter-001',
|
66 |
+
enemyLevel: 5,
|
67 |
+
createdAt: new Date()
|
68 |
+
});
|
69 |
+
|
70 |
+
// IMPORTANT: Return here - don't add shop/health center for first catch
|
71 |
+
await db.encounters.clear();
|
72 |
+
await db.encounters.add(encounters[0]);
|
73 |
+
await markEncountersRefreshed();
|
74 |
+
return await this.getCurrentEncounters();
|
75 |
}
|
76 |
+
|
77 |
+
// Player has piclets - generate normal encounters
|
78 |
+
|
79 |
+
// Always add shop and health center first
|
80 |
encounters.push({
|
81 |
type: EncounterType.SHOP,
|
82 |
title: 'Piclet Shop',
|
|
|
90 |
description: 'Heal your piclets back to full health',
|
91 |
createdAt: new Date()
|
92 |
});
|
93 |
+
|
94 |
+
// Generate wild piclet encounters
|
95 |
+
const wildEncounters = await this.generateWildEncounters();
|
96 |
+
encounters.push(...wildEncounters);
|
97 |
|
98 |
// Clear existing encounters and add new ones
|
99 |
await db.encounters.clear();
|
|
|
168 |
|
169 |
// Catch a wild piclet (for first encounter)
|
170 |
static async catchWildPiclet(encounter: Encounter): Promise<PicletInstance> {
|
171 |
+
// Get the discovered piclet data
|
172 |
+
const discoveredPiclets = await db.monsters.toArray();
|
173 |
+
const picletData = discoveredPiclets.find(p => p.id?.toString() === encounter.picletTypeId) || discoveredPiclets[0];
|
174 |
+
|
175 |
const newPiclet: Omit<PicletInstance, 'id'> = {
|
176 |
typeId: encounter.picletTypeId!,
|
177 |
+
nickname: picletData?.name || 'Starter Piclet',
|
178 |
primaryTypeString: 'normal',
|
179 |
|
180 |
+
// Stats based on level
|
181 |
level: encounter.enemyLevel || 5,
|
182 |
xp: 0,
|
183 |
+
currentHp: 20 + (encounter.enemyLevel || 5) * 4,
|
184 |
+
maxHp: 20 + (encounter.enemyLevel || 5) * 4,
|
185 |
+
attack: 10 + (encounter.enemyLevel || 5) * 2,
|
186 |
+
defense: 10 + (encounter.enemyLevel || 5) * 2,
|
187 |
+
fieldAttack: 10 + (encounter.enemyLevel || 5) * 2,
|
188 |
+
fieldDefense: 10 + (encounter.enemyLevel || 5) * 2,
|
189 |
+
speed: 10 + (encounter.enemyLevel || 5) * 2,
|
190 |
|
191 |
// Base stats
|
192 |
baseHp: 20,
|
|
|
196 |
baseFieldDefense: 10,
|
197 |
baseSpeed: 10,
|
198 |
|
199 |
+
// Battle moves
|
200 |
+
moves: [
|
201 |
+
{
|
202 |
+
name: 'Tackle',
|
203 |
+
type: 'normal',
|
204 |
+
power: 40,
|
205 |
+
accuracy: 100,
|
206 |
+
pp: 35,
|
207 |
+
currentPp: 35,
|
208 |
+
description: 'A physical attack'
|
209 |
+
}
|
210 |
+
],
|
211 |
nature: 'hardy',
|
212 |
|
213 |
// Roster
|
|
|
221 |
role: 'balanced',
|
222 |
variance: 1,
|
223 |
|
224 |
+
// Visuals from discovered data
|
225 |
+
imageUrl: picletData?.imageUrl || `https://storage.googleapis.com/piclodia/${encounter.picletTypeId}.png`,
|
226 |
+
imageData: picletData?.imageData,
|
227 |
+
imageCaption: picletData?.imageCaption || 'A friendly starter piclet',
|
228 |
+
concept: picletData?.concept || 'starter',
|
229 |
+
imagePrompt: picletData?.imagePrompt || 'cute starter monster'
|
230 |
};
|
231 |
|
232 |
const id = await db.picletInstances.add(newPiclet);
|
src/lib/db/resetGame.ts
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { db } from './index';
|
2 |
+
|
3 |
+
// Utility function to reset the game state (useful for testing)
|
4 |
+
export async function resetGameState() {
|
5 |
+
// Clear all game data
|
6 |
+
await db.picletInstances.clear();
|
7 |
+
await db.encounters.clear();
|
8 |
+
await db.gameState.clear();
|
9 |
+
|
10 |
+
// Note: We don't clear monsters as those represent scanned/discovered piclets
|
11 |
+
console.log('Game state reset - all caught piclets and encounters cleared');
|
12 |
+
}
|
13 |
+
|
14 |
+
// Clear only discovered piclets (monsters)
|
15 |
+
export async function clearDiscoveredPiclets() {
|
16 |
+
await db.monsters.clear();
|
17 |
+
console.log('Discovered piclets cleared');
|
18 |
+
}
|
19 |
+
|
20 |
+
// Full reset including discovered piclets
|
21 |
+
export async function fullGameReset() {
|
22 |
+
await resetGameState();
|
23 |
+
await clearDiscoveredPiclets();
|
24 |
+
console.log('Full game reset - all data cleared');
|
25 |
+
}
|
26 |
+
|
27 |
+
// Make functions available globally for debugging
|
28 |
+
if (typeof window !== 'undefined') {
|
29 |
+
(window as any).resetGameState = resetGameState;
|
30 |
+
(window as any).clearDiscoveredPiclets = clearDiscoveredPiclets;
|
31 |
+
(window as any).fullGameReset = fullGameReset;
|
32 |
+
}
|
src/lib/db/schema.ts
CHANGED
@@ -101,6 +101,36 @@ export interface GameState {
|
|
101 |
battlesLost: number;
|
102 |
}
|
103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
// Legacy Monster interface for backward compatibility
|
105 |
export interface Monster {
|
106 |
id?: number;
|
|
|
101 |
battlesLost: number;
|
102 |
}
|
103 |
|
104 |
+
// Battle System Types
|
105 |
+
export enum BattlePhase {
|
106 |
+
INTRO = 'intro',
|
107 |
+
MAIN = 'main',
|
108 |
+
MOVE_SELECT = 'moveSelect',
|
109 |
+
PICLET_SELECT = 'picletSelect',
|
110 |
+
FORCED_SWAP = 'forcedSwap',
|
111 |
+
BATTLE_END = 'battleEnd'
|
112 |
+
}
|
113 |
+
|
114 |
+
export enum ActionView {
|
115 |
+
MAIN = 'main',
|
116 |
+
MOVES = 'moves',
|
117 |
+
PICLETS = 'piclets',
|
118 |
+
ITEMS = 'items',
|
119 |
+
FORCED_SWAP = 'forcedSwap'
|
120 |
+
}
|
121 |
+
|
122 |
+
export interface BattleState {
|
123 |
+
phase: BattlePhase;
|
124 |
+
currentTurn: number;
|
125 |
+
playerPiclet: PicletInstance;
|
126 |
+
enemyPiclet: PicletInstance;
|
127 |
+
isWildBattle: boolean;
|
128 |
+
processingTurn: boolean;
|
129 |
+
battleEnded: boolean;
|
130 |
+
winner?: 'player' | 'enemy';
|
131 |
+
capturedPiclet?: PicletInstance;
|
132 |
+
}
|
133 |
+
|
134 |
// Legacy Monster interface for backward compatibility
|
135 |
export interface Monster {
|
136 |
id?: number;
|
src/main.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import { mount } from 'svelte'
|
2 |
import './app.css'
|
3 |
import App from './App.svelte'
|
|
|
4 |
|
5 |
const app = mount(App, {
|
6 |
target: document.getElementById('app')!,
|
|
|
1 |
import { mount } from 'svelte'
|
2 |
import './app.css'
|
3 |
import App from './App.svelte'
|
4 |
+
import './lib/db/resetGame' // Import to make reset functions available
|
5 |
|
6 |
const app = mount(App, {
|
7 |
target: document.getElementById('app')!,
|
src/tests/encounterService.test.ts
ADDED
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { describe, it, expect, beforeEach } from 'vitest';
|
2 |
+
import { EncounterService } from '../lib/db/encounterService';
|
3 |
+
import { db } from '../lib/db';
|
4 |
+
import { EncounterType } from '../lib/db/schema';
|
5 |
+
import type { Monster, PicletInstance } from '../lib/db/schema';
|
6 |
+
|
7 |
+
describe('EncounterService', () => {
|
8 |
+
beforeEach(async () => {
|
9 |
+
// Clear all data before each test
|
10 |
+
await db.monsters.clear();
|
11 |
+
await db.picletInstances.clear();
|
12 |
+
await db.encounters.clear();
|
13 |
+
await db.gameState.clear();
|
14 |
+
});
|
15 |
+
|
16 |
+
describe('generateEncounters', () => {
|
17 |
+
it('should return empty array when no piclets are discovered', async () => {
|
18 |
+
// Arrange - ensure database is empty
|
19 |
+
const monsterCount = await db.monsters.count();
|
20 |
+
const picletCount = await db.picletInstances.count();
|
21 |
+
expect(monsterCount).toBe(0);
|
22 |
+
expect(picletCount).toBe(0);
|
23 |
+
|
24 |
+
// Act
|
25 |
+
const encounters = await EncounterService.generateEncounters();
|
26 |
+
|
27 |
+
// Assert
|
28 |
+
expect(encounters).toHaveLength(0);
|
29 |
+
|
30 |
+
// Verify encounters table is also empty
|
31 |
+
const dbEncounters = await db.encounters.toArray();
|
32 |
+
expect(dbEncounters).toHaveLength(0);
|
33 |
+
});
|
34 |
+
|
35 |
+
it('should return only "Your First Piclet!" when piclets are discovered but not caught', async () => {
|
36 |
+
// Arrange - add a discovered piclet (monster)
|
37 |
+
const testMonster: Omit<Monster, 'id'> = {
|
38 |
+
name: 'Test Piclet',
|
39 |
+
imageUrl: 'https://test.com/piclet.png',
|
40 |
+
concept: 'test',
|
41 |
+
imagePrompt: 'test prompt',
|
42 |
+
imageCaption: 'test caption',
|
43 |
+
createdAt: new Date()
|
44 |
+
};
|
45 |
+
await db.monsters.add(testMonster);
|
46 |
+
|
47 |
+
// Act
|
48 |
+
const encounters = await EncounterService.generateEncounters();
|
49 |
+
|
50 |
+
// Assert
|
51 |
+
expect(encounters).toHaveLength(1);
|
52 |
+
expect(encounters[0].type).toBe(EncounterType.WILD_PICLET);
|
53 |
+
expect(encounters[0].title).toBe('Your First Piclet!');
|
54 |
+
expect(encounters[0].description).toBe('A friendly piclet appears! This one seems easy to catch.');
|
55 |
+
expect(encounters[0].enemyLevel).toBe(5);
|
56 |
+
});
|
57 |
+
|
58 |
+
it('should return shop, health center, and wild encounters when player has caught piclets', async () => {
|
59 |
+
// Arrange - add a caught piclet
|
60 |
+
const testPiclet: Omit<PicletInstance, 'id'> = {
|
61 |
+
typeId: 'test-001',
|
62 |
+
nickname: 'Testy',
|
63 |
+
primaryTypeString: 'normal',
|
64 |
+
level: 5,
|
65 |
+
xp: 0,
|
66 |
+
currentHp: 20,
|
67 |
+
maxHp: 20,
|
68 |
+
attack: 10,
|
69 |
+
defense: 10,
|
70 |
+
fieldAttack: 10,
|
71 |
+
fieldDefense: 10,
|
72 |
+
speed: 10,
|
73 |
+
baseHp: 20,
|
74 |
+
baseAttack: 10,
|
75 |
+
baseDefense: 10,
|
76 |
+
baseFieldAttack: 10,
|
77 |
+
baseFieldDefense: 10,
|
78 |
+
baseSpeed: 10,
|
79 |
+
moves: [],
|
80 |
+
nature: 'hardy',
|
81 |
+
isInRoster: true,
|
82 |
+
rosterPosition: 0,
|
83 |
+
caughtAt: new Date(),
|
84 |
+
bst: 60,
|
85 |
+
tier: 'common',
|
86 |
+
role: 'balanced',
|
87 |
+
variance: 1,
|
88 |
+
imageUrl: 'https://test.com/piclet.png',
|
89 |
+
imageCaption: 'Test',
|
90 |
+
concept: 'test',
|
91 |
+
imagePrompt: 'test'
|
92 |
+
};
|
93 |
+
await db.picletInstances.add(testPiclet);
|
94 |
+
|
95 |
+
// Act
|
96 |
+
const encounters = await EncounterService.generateEncounters();
|
97 |
+
|
98 |
+
// Assert
|
99 |
+
expect(encounters.length).toBeGreaterThanOrEqual(4); // At least shop, health center, and 2 wild
|
100 |
+
|
101 |
+
// Check for required encounter types
|
102 |
+
const encounterTypes = encounters.map(e => e.type);
|
103 |
+
expect(encounterTypes).toContain(EncounterType.SHOP);
|
104 |
+
expect(encounterTypes).toContain(EncounterType.HEALTH_CENTER);
|
105 |
+
|
106 |
+
// Count wild encounters
|
107 |
+
const wildCount = encounters.filter(e => e.type === EncounterType.WILD_PICLET).length;
|
108 |
+
expect(wildCount).toBeGreaterThanOrEqual(2);
|
109 |
+
expect(wildCount).toBeLessThanOrEqual(3);
|
110 |
+
|
111 |
+
// Verify shop encounter details
|
112 |
+
const shopEncounter = encounters.find(e => e.type === EncounterType.SHOP);
|
113 |
+
expect(shopEncounter?.title).toBe('Piclet Shop');
|
114 |
+
|
115 |
+
// Verify health center encounter details
|
116 |
+
const healthEncounter = encounters.find(e => e.type === EncounterType.HEALTH_CENTER);
|
117 |
+
expect(healthEncounter?.title).toBe('Health Center');
|
118 |
+
});
|
119 |
+
|
120 |
+
it('should not include shop/health center with first catch encounter', async () => {
|
121 |
+
// Arrange - add only a discovered piclet, no caught piclets
|
122 |
+
await db.monsters.add({
|
123 |
+
name: 'Discovered Piclet',
|
124 |
+
imageUrl: 'https://test.com/discovered.png',
|
125 |
+
concept: 'discovered',
|
126 |
+
imagePrompt: 'discovered prompt',
|
127 |
+
imageCaption: 'discovered caption',
|
128 |
+
createdAt: new Date()
|
129 |
+
});
|
130 |
+
|
131 |
+
// Ensure no caught piclets
|
132 |
+
const caughtCount = await db.picletInstances.count();
|
133 |
+
expect(caughtCount).toBe(0);
|
134 |
+
|
135 |
+
// Act
|
136 |
+
const encounters = await EncounterService.generateEncounters();
|
137 |
+
|
138 |
+
// Assert - should only have the first catch encounter
|
139 |
+
expect(encounters).toHaveLength(1);
|
140 |
+
expect(encounters[0].title).toBe('Your First Piclet!');
|
141 |
+
|
142 |
+
// Should NOT have shop or health center
|
143 |
+
const hasShop = encounters.some(e => e.type === EncounterType.SHOP);
|
144 |
+
const hasHealthCenter = encounters.some(e => e.type === EncounterType.HEALTH_CENTER);
|
145 |
+
expect(hasShop).toBe(false);
|
146 |
+
expect(hasHealthCenter).toBe(false);
|
147 |
+
});
|
148 |
+
});
|
149 |
+
|
150 |
+
describe('shouldRefreshEncounters', () => {
|
151 |
+
it('should return true after 2 hours', async () => {
|
152 |
+
// Arrange - create game state with old refresh time
|
153 |
+
const twoHoursAgo = new Date(Date.now() - (2.5 * 60 * 60 * 1000));
|
154 |
+
await db.gameState.add({
|
155 |
+
lastEncounterRefresh: twoHoursAgo,
|
156 |
+
lastPlayed: new Date(),
|
157 |
+
progressPoints: 0,
|
158 |
+
trainersDefeated: 0,
|
159 |
+
picletsCapured: 0,
|
160 |
+
battlesLost: 0
|
161 |
+
});
|
162 |
+
|
163 |
+
// Act
|
164 |
+
const shouldRefresh = await EncounterService.shouldRefreshEncounters();
|
165 |
+
|
166 |
+
// Assert
|
167 |
+
expect(shouldRefresh).toBe(true);
|
168 |
+
});
|
169 |
+
|
170 |
+
it('should return false within 2 hours', async () => {
|
171 |
+
// Arrange - create game state with recent refresh time
|
172 |
+
const oneHourAgo = new Date(Date.now() - (1 * 60 * 60 * 1000));
|
173 |
+
await db.gameState.add({
|
174 |
+
lastEncounterRefresh: oneHourAgo,
|
175 |
+
lastPlayed: new Date(),
|
176 |
+
progressPoints: 0,
|
177 |
+
trainersDefeated: 0,
|
178 |
+
picletsCapured: 0,
|
179 |
+
battlesLost: 0
|
180 |
+
});
|
181 |
+
|
182 |
+
// Act
|
183 |
+
const shouldRefresh = await EncounterService.shouldRefreshEncounters();
|
184 |
+
|
185 |
+
// Assert
|
186 |
+
expect(shouldRefresh).toBe(false);
|
187 |
+
});
|
188 |
+
});
|
189 |
+
});
|
src/tests/setup.ts
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import 'fake-indexeddb/auto';
|
2 |
+
import { beforeEach } from 'vitest';
|
3 |
+
import { db } from '../lib/db';
|
4 |
+
|
5 |
+
// Reset database before each test
|
6 |
+
beforeEach(async () => {
|
7 |
+
// Clear all tables
|
8 |
+
await db.monsters.clear();
|
9 |
+
await db.picletInstances.clear();
|
10 |
+
await db.encounters.clear();
|
11 |
+
await db.gameState.clear();
|
12 |
+
});
|
vitest.config.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'vitest/config';
|
2 |
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
3 |
+
|
4 |
+
export default defineConfig({
|
5 |
+
plugins: [svelte({ hot: !process.env.VITEST })],
|
6 |
+
test: {
|
7 |
+
globals: true,
|
8 |
+
environment: 'happy-dom',
|
9 |
+
setupFiles: ['./src/tests/setup.ts'],
|
10 |
+
},
|
11 |
+
resolve: {
|
12 |
+
alias: {
|
13 |
+
$lib: '/src/lib',
|
14 |
+
},
|
15 |
+
},
|
16 |
+
});
|