Fraser commited on
Commit
7428b13
·
1 Parent(s): 465b043
README.md CHANGED
@@ -12,7 +12,127 @@ hf_oauth_scopes:
12
  - inference-api
13
  ---
14
 
15
- https://fraser-piclets.static.hf.space/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: 909c22d35be7a34077a3aa88dcd90a5a0a643afbbd80a0a1e3dec182b8cf3ebd
  • Pointer size: 132 Bytes
  • Size of remote file: 1.23 MB
public/images/environments/cave.png ADDED

Git LFS Details

  • SHA256: 4946eff621aa3f1bdffa60cd9f105287a6f663cf0080309ebcac9903abc4e576
  • Pointer size: 132 Bytes
  • Size of remote file: 1.58 MB
public/images/environments/dinner.png ADDED

Git LFS Details

  • SHA256: eb3cfb052b0b470b4483fb39c45029290cd814748b739f39f5ec2372c7056df0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.73 MB
public/images/environments/road.png ADDED

Git LFS Details

  • SHA256: 80ac256f0347b491f44437581c2d5122bb0e4d3a73bdc2205fd54bbe0df56ab0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.48 MB
public/images/environments/ruins.png ADDED

Git LFS Details

  • SHA256: 0fee14fb32364c129a3a11dbcfc313eeba42bf11ee4295278f7a95aa9d584a56
  • Pointer size: 132 Bytes
  • Size of remote file: 1.68 MB
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 - would start battle
65
- alert('Battle system coming soon!');
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">🗺️</div>
159
- <h2>No Encounters Available</h2>
160
- <p>New encounters will appear soon!</p>
161
- <button class="refresh-button" on:click={loadEncounters}>
162
- Refresh
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">?</div>
 
 
 
 
 
 
 
 
 
 
 
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
- .refresh-button {
377
  margin-top: 1rem;
378
  padding: 0.75rem 1.5rem;
379
- background: #4caf50;
380
  color: white;
381
  border: none;
382
  border-radius: 8px;
@@ -386,7 +536,7 @@
386
  transition: background 0.2s ease;
387
  }
388
 
389
- .refresh-button:hover {
390
- background: #45a049;
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 needs first catch encounter
44
  const playerPiclets = await db.picletInstances.toArray();
 
45
  if (playerPiclets.length === 0) {
46
- encounters.push(await this.createFirstCatchEncounter());
47
- } else {
48
- // Generate wild piclet encounters
49
- const wildEncounters = await this.generateWildEncounters();
50
- encounters.push(...wildEncounters);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
-
53
- // Always add shop and health center
 
 
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
- // TODO: Implement actual piclet catching logic
142
- // For now, create a placeholder instance
 
 
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
- imageCaption: 'A friendly starter piclet',
185
- concept: 'starter',
186
- imagePrompt: 'cute starter monster'
 
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
+ });