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 |
+
});
|