Fraser commited on
Commit
4c208f2
·
1 Parent(s): ac7c05c

closer to battle running

Browse files
src/lib/battle-engine/types.ts CHANGED
@@ -3,9 +3,10 @@
3
  * Based on battle_system_design.md specification
4
  */
5
 
6
- import { PicletType, AttackType, TypeEffectiveness } from '../types/picletTypes';
7
 
8
- export { PicletType, AttackType, TypeEffectiveness };
 
9
 
10
  export type Tier = 'low' | 'medium' | 'high' | 'legendary';
11
 
 
3
  * Based on battle_system_design.md specification
4
  */
5
 
6
+ import { PicletType, AttackType, type TypeEffectiveness } from '../types/picletTypes';
7
 
8
+ export { PicletType, AttackType };
9
+ export type { TypeEffectiveness };
10
 
11
  export type Tier = 'low' | 'medium' | 'high' | 'legendary';
12
 
src/lib/components/Battle/ActionButtons.svelte CHANGED
@@ -1,12 +1,14 @@
1
  <script lang="ts">
2
  import ActionViewSelector, { type ActionView } from './ActionViewSelector.svelte';
3
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
 
4
 
5
  export let isWildBattle: boolean;
6
  export let playerPiclet: PicletInstance;
7
  export let enemyPiclet: PicletInstance | null = null;
8
  export let availablePiclets: PicletInstance[] = [];
9
  export let processingTurn: boolean = false;
 
10
  export let onAction: (action: string) => void;
11
  export let onMoveSelect: (move: BattleMove) => void = () => {};
12
  export let onPicletSelect: (piclet: PicletInstance) => void = () => {};
@@ -46,6 +48,7 @@
46
  {availablePiclets}
47
  {enemyPiclet}
48
  {isWildBattle}
 
49
  onMoveSelected={handleMoveSelected}
50
  onPicletSelected={handlePicletSelected}
51
  onCaptureAttempt={handleCaptureAttempt}
 
1
  <script lang="ts">
2
  import ActionViewSelector, { type ActionView } from './ActionViewSelector.svelte';
3
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
4
+ import type { BattleState } from '$lib/battle-engine/types';
5
 
6
  export let isWildBattle: boolean;
7
  export let playerPiclet: PicletInstance;
8
  export let enemyPiclet: PicletInstance | null = null;
9
  export let availablePiclets: PicletInstance[] = [];
10
  export let processingTurn: boolean = false;
11
+ export let battleState: BattleState | undefined = undefined;
12
  export let onAction: (action: string) => void;
13
  export let onMoveSelect: (move: BattleMove) => void = () => {};
14
  export let onPicletSelect: (piclet: PicletInstance) => void = () => {};
 
48
  {availablePiclets}
49
  {enemyPiclet}
50
  {isWildBattle}
51
+ {battleState}
52
  onMoveSelected={handleMoveSelected}
53
  onPicletSelected={handlePicletSelected}
54
  onCaptureAttempt={handleCaptureAttempt}
src/lib/components/Battle/ActionViewSelector.svelte CHANGED
@@ -1,7 +1,10 @@
 
 
 
 
1
  <script lang="ts">
2
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
3
-
4
- export type ActionView = 'main' | 'moves' | 'piclets' | 'items' | 'stats' | 'forcedSwap';
5
 
6
  export let currentView: ActionView = 'main';
7
  export let onViewChange: (view: ActionView) => void;
@@ -9,12 +12,16 @@
9
  export let availablePiclets: PicletInstance[] = [];
10
  export let enemyPiclet: PicletInstance | null = null;
11
  export let isWildBattle: boolean = false;
 
12
  export let onMoveSelected: (move: BattleMove) => void = () => {};
13
  export let onPicletSelected: (piclet: PicletInstance) => void = () => {};
14
  export let onCaptureAttempt: () => void = () => {};
15
  export let currentPicletId: number | null = null;
16
  export let processingTurn: boolean = false;
17
 
 
 
 
18
  // Main action items
19
  const actions = [
20
  { title: 'Act', icon: '⚔️', view: 'moves' as ActionView },
@@ -59,8 +66,10 @@
59
  <div class="sub-view-content">
60
  {#if currentView === 'moves'}
61
  <div class="sub-view-list">
62
- {#each moves as move}
63
  {@const isDisabled = move.currentPp <= 0}
 
 
64
  <button
65
  class="sub-item move-item"
66
  on:click={() => !isDisabled && onMoveSelected(move)}
@@ -68,15 +77,44 @@
68
  >
69
  <span class="type-emoji" class:disabled={isDisabled}>
70
  {move.type === 'normal' ? '⚪' :
71
- move.type === 'status' ? '🔮' :
72
- move.type === 'special' ? '' : '❓'}
 
 
 
 
 
 
 
 
73
  </span>
74
  <div class="move-info">
75
- <div class="move-name" class:disabled={isDisabled}>{move.name}</div>
76
- <div class="move-desc" class:disabled={isDisabled}>{move.description}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  </div>
78
- <div class="move-pp" class:disabled={isDisabled}>
79
- PP: {move.currentPp}/{move.pp}
 
 
 
80
  </div>
81
  </button>
82
  {/each}
@@ -335,6 +373,42 @@
335
  color: #c7c7cc;
336
  }
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  .move-pp {
339
  font-size: 12px;
340
  padding: 4px 8px;
@@ -344,9 +418,13 @@
344
  white-space: nowrap;
345
  }
346
 
347
- .move-pp.disabled {
348
- background: #f2f2f7;
349
- color: #8e8e93;
 
 
 
 
350
  }
351
 
352
  /* Piclet items */
 
1
+ <script context="module" lang="ts">
2
+ export type ActionView = 'main' | 'moves' | 'piclets' | 'items' | 'stats' | 'forcedSwap';
3
+ </script>
4
+
5
  <script lang="ts">
6
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
+ import type { BattleState } from '$lib/battle-engine/types';
 
8
 
9
  export let currentView: ActionView = 'main';
10
  export let onViewChange: (view: ActionView) => void;
 
12
  export let availablePiclets: PicletInstance[] = [];
13
  export let enemyPiclet: PicletInstance | null = null;
14
  export let isWildBattle: boolean = false;
15
+ export let battleState: BattleState | undefined = undefined;
16
  export let onMoveSelected: (move: BattleMove) => void = () => {};
17
  export let onPicletSelected: (piclet: PicletInstance) => void = () => {};
18
  export let onCaptureAttempt: () => void = () => {};
19
  export let currentPicletId: number | null = null;
20
  export let processingTurn: boolean = false;
21
 
22
+ // Enhanced move information from battle state
23
+ $: enhancedMoves = battleState?.playerPiclet?.moves || [];
24
+
25
  // Main action items
26
  const actions = [
27
  { title: 'Act', icon: '⚔️', view: 'moves' as ActionView },
 
66
  <div class="sub-view-content">
67
  {#if currentView === 'moves'}
68
  <div class="sub-view-list">
69
+ {#each moves as move, index}
70
  {@const isDisabled = move.currentPp <= 0}
71
+ {@const enhancedMove = enhancedMoves[index]}
72
+ {@const battleMove = enhancedMove?.move}
73
  <button
74
  class="sub-item move-item"
75
  on:click={() => !isDisabled && onMoveSelected(move)}
 
77
  >
78
  <span class="type-emoji" class:disabled={isDisabled}>
79
  {move.type === 'normal' ? '⚪' :
80
+ move.type === 'beast' ? '🦁' :
81
+ move.type === 'bug' ? '🐛' :
82
+ move.type === 'aquatic' ? '🌊' :
83
+ move.type === 'flora' ? '🌿' :
84
+ move.type === 'mineral' ? '💎' :
85
+ move.type === 'space' ? '🌌' :
86
+ move.type === 'machina' ? '⚙️' :
87
+ move.type === 'structure' ? '🏗️' :
88
+ move.type === 'culture' ? '🎭' :
89
+ move.type === 'cuisine' ? '🍽️' : '❓'}
90
  </span>
91
  <div class="move-info">
92
+ <div class="move-name" class:disabled={isDisabled}>
93
+ {move.name}
94
+ {#if battleMove?.power > 0}
95
+ <span class="move-power">⚡{battleMove.power}</span>
96
+ {/if}
97
+ </div>
98
+ <div class="move-desc" class:disabled={isDisabled}>
99
+ {#if battleMove?.effects && battleMove.effects.length > 0}
100
+ {battleMove.effects.length} effects • {move.description}
101
+ {:else}
102
+ {move.description}
103
+ {/if}
104
+ </div>
105
+ {#if battleMove?.flags && battleMove.flags.length > 0}
106
+ <div class="move-flags">
107
+ {#each battleMove.flags as flag}
108
+ <span class="flag-badge">{flag}</span>
109
+ {/each}
110
+ </div>
111
+ {/if}
112
  </div>
113
+ <div class="move-stats" class:disabled={isDisabled}>
114
+ <div class="move-pp">PP: {move.currentPp}/{move.pp}</div>
115
+ {#if battleMove}
116
+ <div class="move-accuracy">Acc: {battleMove.accuracy}%</div>
117
+ {/if}
118
  </div>
119
  </button>
120
  {/each}
 
373
  color: #c7c7cc;
374
  }
375
 
376
+ .move-power {
377
+ font-size: 11px;
378
+ color: #ff6b35;
379
+ font-weight: bold;
380
+ margin-left: 6px;
381
+ }
382
+
383
+ .move-flags {
384
+ display: flex;
385
+ gap: 4px;
386
+ margin-top: 4px;
387
+ flex-wrap: wrap;
388
+ }
389
+
390
+ .flag-badge {
391
+ font-size: 10px;
392
+ padding: 2px 6px;
393
+ background: #e3f2fd;
394
+ color: #1976d2;
395
+ border-radius: 8px;
396
+ font-weight: 500;
397
+ }
398
+
399
+ .move-stats {
400
+ display: flex;
401
+ flex-direction: column;
402
+ gap: 2px;
403
+ align-items: flex-end;
404
+ }
405
+
406
+ .move-stats.disabled .move-pp,
407
+ .move-stats.disabled .move-accuracy {
408
+ background: #f2f2f7;
409
+ color: #8e8e93;
410
+ }
411
+
412
  .move-pp {
413
  font-size: 12px;
414
  padding: 4px 8px;
 
418
  white-space: nowrap;
419
  }
420
 
421
+ .move-accuracy {
422
+ font-size: 11px;
423
+ padding: 2px 6px;
424
+ background: #e8f5e8;
425
+ color: #2e7d32;
426
+ border-radius: 8px;
427
+ white-space: nowrap;
428
  }
429
 
430
  /* Piclet items */
src/lib/components/Battle/BattleControls.svelte CHANGED
@@ -1,5 +1,6 @@
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
 
3
  import ActionButtons from './ActionButtons.svelte';
4
  import TypewriterText from './TypewriterText.svelte';
5
 
@@ -11,6 +12,7 @@
11
  export let playerPiclet: PicletInstance;
12
  export let enemyPiclet: PicletInstance;
13
  export let rosterPiclets: PicletInstance[] = [];
 
14
  export let onAction: (action: string) => void;
15
  export let onMoveSelect: (move: any) => void;
16
  export let onPicletSelect: (piclet: PicletInstance) => void;
@@ -34,6 +36,7 @@
34
  {enemyPiclet}
35
  {availablePiclets}
36
  {processingTurn}
 
37
  {onAction}
38
  {onMoveSelect}
39
  {onPicletSelect}
 
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
3
+ import type { BattleState } from '$lib/battle-engine/types';
4
  import ActionButtons from './ActionButtons.svelte';
5
  import TypewriterText from './TypewriterText.svelte';
6
 
 
12
  export let playerPiclet: PicletInstance;
13
  export let enemyPiclet: PicletInstance;
14
  export let rosterPiclets: PicletInstance[] = [];
15
+ export let battleState: BattleState | undefined = undefined;
16
  export let onAction: (action: string) => void;
17
  export let onMoveSelect: (move: any) => void;
18
  export let onPicletSelect: (piclet: PicletInstance) => void;
 
36
  {enemyPiclet}
37
  {availablePiclets}
38
  {processingTurn}
39
+ {battleState}
40
  {onAction}
41
  {onMoveSelect}
42
  {onPicletSelect}
src/lib/components/Battle/BattleField.svelte CHANGED
@@ -2,13 +2,17 @@
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
  // Animation states
14
  let playerVisible = false;
@@ -47,6 +51,11 @@
47
  {/if}
48
 
49
  <div class="battle-content">
 
 
 
 
 
50
  <!-- Enemy Row -->
51
  <div class="enemy-row">
52
  <div class="enemy-stack" class:intro-animations={showIntro}>
@@ -63,18 +72,30 @@
63
  class="piclet-image enemy-image"
64
  src={enemyPiclet.imageData || enemyPiclet.imageUrl}
65
  alt={enemyPiclet.nickname}
66
- on:error={(e) => e.currentTarget.src = 'https://via.placeholder.com/120x120?text=Piclet'}
 
 
 
67
  />
68
  <img
69
  class="platform enemy-platform"
70
  src="/assets/grass.PNG"
71
  alt="Platform"
72
  on:error={(e) => {
73
- e.currentTarget.style.display = 'none';
74
- e.currentTarget.nextElementSibling.style.display = 'block';
 
 
75
  }}
76
  />
77
  <div class="platform-fallback enemy-platform-fallback" style="display: none;"></div>
 
 
 
 
 
 
 
78
  </div>
79
  {/if}
80
  </div>
@@ -91,18 +112,30 @@
91
  class="piclet-image player-image"
92
  src={playerPiclet.imageData || playerPiclet.imageUrl}
93
  alt={playerPiclet.nickname}
94
- on:error={(e) => e.currentTarget.src = 'https://via.placeholder.com/120x120?text=Piclet'}
 
 
 
95
  />
96
  <img
97
  class="platform player-platform"
98
  src="/assets/grass.PNG"
99
  alt="Platform"
100
  on:error={(e) => {
101
- e.currentTarget.style.display = 'none';
102
- e.currentTarget.nextElementSibling.style.display = 'block';
 
 
103
  }}
104
  />
105
  <div class="platform-fallback player-platform-fallback" style="display: none;"></div>
 
 
 
 
 
 
 
106
  </div>
107
  {/if}
108
 
@@ -269,6 +302,21 @@
269
  flex: 1;
270
  }
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  /* Animations */
273
  .enemy-piclet-wrapper {
274
  animation-fill-mode: both;
 
2
  import { onMount } from 'svelte';
3
  import { fade } from 'svelte/transition';
4
  import type { PicletInstance } from '$lib/db/schema';
5
+ import type { BattleState } from '$lib/battle-engine/types';
6
  import PicletInfo from './PicletInfo.svelte';
7
+ import StatusEffectIndicator from './StatusEffectIndicator.svelte';
8
+ import FieldEffectIndicator from './FieldEffectIndicator.svelte';
9
 
10
  export let playerPiclet: PicletInstance;
11
  export let enemyPiclet: PicletInstance;
12
  export let playerHpPercentage: number;
13
  export let enemyHpPercentage: number;
14
  export let showIntro: boolean = false;
15
+ export let battleState: BattleState | undefined = undefined;
16
 
17
  // Animation states
18
  let playerVisible = false;
 
51
  {/if}
52
 
53
  <div class="battle-content">
54
+ <!-- Field Effects Display -->
55
+ {#if battleState?.fieldEffects}
56
+ <FieldEffectIndicator fieldEffects={battleState.fieldEffects} />
57
+ {/if}
58
+
59
  <!-- Enemy Row -->
60
  <div class="enemy-row">
61
  <div class="enemy-stack" class:intro-animations={showIntro}>
 
72
  class="piclet-image enemy-image"
73
  src={enemyPiclet.imageData || enemyPiclet.imageUrl}
74
  alt={enemyPiclet.nickname}
75
+ on:error={(e) => {
76
+ const target = e.currentTarget as HTMLImageElement;
77
+ target.src = 'https://via.placeholder.com/120x120?text=Piclet';
78
+ }}
79
  />
80
  <img
81
  class="platform enemy-platform"
82
  src="/assets/grass.PNG"
83
  alt="Platform"
84
  on:error={(e) => {
85
+ const target = e.currentTarget as HTMLImageElement;
86
+ const nextSibling = target.nextElementSibling as HTMLElement;
87
+ target.style.display = 'none';
88
+ if (nextSibling) nextSibling.style.display = 'block';
89
  }}
90
  />
91
  <div class="platform-fallback enemy-platform-fallback" style="display: none;"></div>
92
+
93
+ <!-- Enemy Status Effects -->
94
+ {#if battleState?.opponentPiclet?.statusEffects}
95
+ <div class="enemy-status-effects">
96
+ <StatusEffectIndicator statusEffects={battleState.opponentPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} />
97
+ </div>
98
+ {/if}
99
  </div>
100
  {/if}
101
  </div>
 
112
  class="piclet-image player-image"
113
  src={playerPiclet.imageData || playerPiclet.imageUrl}
114
  alt={playerPiclet.nickname}
115
+ on:error={(e) => {
116
+ const target = e.currentTarget as HTMLImageElement;
117
+ target.src = 'https://via.placeholder.com/120x120?text=Piclet';
118
+ }}
119
  />
120
  <img
121
  class="platform player-platform"
122
  src="/assets/grass.PNG"
123
  alt="Platform"
124
  on:error={(e) => {
125
+ const target = e.currentTarget as HTMLImageElement;
126
+ const nextSibling = target.nextElementSibling as HTMLElement;
127
+ target.style.display = 'none';
128
+ if (nextSibling) nextSibling.style.display = 'block';
129
  }}
130
  />
131
  <div class="platform-fallback player-platform-fallback" style="display: none;"></div>
132
+
133
+ <!-- Player Status Effects -->
134
+ {#if battleState?.playerPiclet?.statusEffects}
135
+ <div class="player-status-effects">
136
+ <StatusEffectIndicator statusEffects={battleState.playerPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} />
137
+ </div>
138
+ {/if}
139
  </div>
140
  {/if}
141
 
 
302
  flex: 1;
303
  }
304
 
305
+ /* Status Effects Positioning */
306
+ .enemy-status-effects {
307
+ position: absolute;
308
+ top: -10px;
309
+ right: -10px;
310
+ z-index: 5;
311
+ }
312
+
313
+ .player-status-effects {
314
+ position: absolute;
315
+ bottom: -10px;
316
+ left: -10px;
317
+ z-index: 5;
318
+ }
319
+
320
  /* Animations */
321
  .enemy-piclet-wrapper {
322
  animation-fill-mode: both;
src/lib/components/Battle/FieldEffectIndicator.svelte ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let fieldEffects: Array<{ effect: string; turnsRemaining?: number; side?: string }> = [];
3
+
4
+ function getFieldEffectColor(effect: string): string {
5
+ if (effect.includes('weather')) return '#4dabf7';
6
+ if (effect.includes('terrain')) return '#51cf66';
7
+ if (effect.includes('hazard')) return '#ff6b6b';
8
+ return '#868e96';
9
+ }
10
+
11
+ function getFieldEffectIcon(effect: string): string {
12
+ if (effect.includes('storm')) return '⛈️';
13
+ if (effect.includes('rain')) return '🌧️';
14
+ if (effect.includes('sun')) return '☀️';
15
+ if (effect.includes('snow')) return '🌨️';
16
+ if (effect.includes('spikes')) return '⚡';
17
+ if (effect.includes('reflect')) return '🛡️';
18
+ return '🌀';
19
+ }
20
+ </script>
21
+
22
+ {#if fieldEffects.length > 0}
23
+ <div class="field-effects">
24
+ <div class="field-effects-header">Field Effects</div>
25
+ {#each fieldEffects as effect}
26
+ <div
27
+ class="field-effect"
28
+ style="background-color: {getFieldEffectColor(effect.effect)}"
29
+ title="{effect.effect}{effect.turnsRemaining ? ` (${effect.turnsRemaining} turns)` : ''}"
30
+ >
31
+ <span class="effect-icon">{getFieldEffectIcon(effect.effect)}</span>
32
+ <span class="effect-name">{effect.effect}</span>
33
+ {#if effect.turnsRemaining}
34
+ <span class="turns-remaining">{effect.turnsRemaining}</span>
35
+ {/if}
36
+ {#if effect.side}
37
+ <span class="effect-side">({effect.side})</span>
38
+ {/if}
39
+ </div>
40
+ {/each}
41
+ </div>
42
+ {/if}
43
+
44
+ <style>
45
+ .field-effects {
46
+ position: absolute;
47
+ top: 10px;
48
+ left: 50%;
49
+ transform: translateX(-50%);
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: center;
53
+ gap: 4px;
54
+ z-index: 10;
55
+ }
56
+
57
+ .field-effects-header {
58
+ background: rgba(0, 0, 0, 0.7);
59
+ color: white;
60
+ padding: 2px 8px;
61
+ border-radius: 12px;
62
+ font-size: 0.7rem;
63
+ font-weight: bold;
64
+ }
65
+
66
+ .field-effect {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 4px;
70
+ padding: 3px 8px;
71
+ border-radius: 12px;
72
+ color: white;
73
+ font-size: 0.7rem;
74
+ font-weight: bold;
75
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
76
+ background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
77
+ animation: shimmer 2s infinite;
78
+ }
79
+
80
+ .effect-icon {
81
+ font-size: 0.8rem;
82
+ }
83
+
84
+ .effect-name {
85
+ font-size: 0.6rem;
86
+ text-transform: capitalize;
87
+ }
88
+
89
+ .turns-remaining {
90
+ background: rgba(255, 255, 255, 0.3);
91
+ border-radius: 8px;
92
+ padding: 1px 4px;
93
+ font-size: 0.6rem;
94
+ }
95
+
96
+ .effect-side {
97
+ font-size: 0.6rem;
98
+ opacity: 0.8;
99
+ }
100
+
101
+ @keyframes shimmer {
102
+ 0%, 100% { background-position: -100% 0; }
103
+ 50% { background-position: 100% 0; }
104
+ }
105
+ </style>
src/lib/components/Battle/StatusEffectIndicator.svelte ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { StatusEffect } from '$lib/battle-engine/types';
3
+
4
+ export let statusEffects: { type: StatusEffect; turnsLeft?: number }[] = [];
5
+
6
+ function getStatusColor(status: StatusEffect): string {
7
+ switch (status) {
8
+ case 'burn': return '#ff6b6b';
9
+ case 'freeze': return '#74c0fc';
10
+ case 'paralyze': return '#ffd43b';
11
+ case 'poison': return '#9775fa';
12
+ case 'sleep': return '#868e96';
13
+ case 'confuse': return '#ff8cc8';
14
+ default: return '#495057';
15
+ }
16
+ }
17
+
18
+ function getStatusIcon(status: StatusEffect): string {
19
+ switch (status) {
20
+ case 'burn': return '🔥';
21
+ case 'freeze': return '❄️';
22
+ case 'paralyze': return '⚡';
23
+ case 'poison': return '☠️';
24
+ case 'sleep': return '💤';
25
+ case 'confuse': return '😵';
26
+ default: return '?';
27
+ }
28
+ }
29
+ </script>
30
+
31
+ {#if statusEffects.length > 0}
32
+ <div class="status-effects">
33
+ {#each statusEffects as effect}
34
+ <div
35
+ class="status-effect"
36
+ style="background-color: {getStatusColor(effect.type)}"
37
+ title="{effect.type}{effect.turnsLeft ? ` (${effect.turnsLeft} turns)` : ''}"
38
+ >
39
+ <span class="status-icon">{getStatusIcon(effect.type)}</span>
40
+ <span class="status-name">{effect.type.toUpperCase()}</span>
41
+ {#if effect.turnsLeft}
42
+ <span class="turns-left">{effect.turnsLeft}</span>
43
+ {/if}
44
+ </div>
45
+ {/each}
46
+ </div>
47
+ {/if}
48
+
49
+ <style>
50
+ .status-effects {
51
+ display: flex;
52
+ gap: 4px;
53
+ flex-wrap: wrap;
54
+ margin: 4px 0;
55
+ }
56
+
57
+ .status-effect {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 2px;
61
+ padding: 2px 6px;
62
+ border-radius: 12px;
63
+ color: white;
64
+ font-size: 0.7rem;
65
+ font-weight: bold;
66
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
67
+ animation: pulse 1.5s infinite;
68
+ }
69
+
70
+ .status-icon {
71
+ font-size: 0.8rem;
72
+ }
73
+
74
+ .status-name {
75
+ font-size: 0.6rem;
76
+ }
77
+
78
+ .turns-left {
79
+ background: rgba(255, 255, 255, 0.3);
80
+ border-radius: 8px;
81
+ padding: 1px 4px;
82
+ font-size: 0.6rem;
83
+ margin-left: 2px;
84
+ }
85
+
86
+ @keyframes pulse {
87
+ 0%, 100% { opacity: 1; }
88
+ 50% { opacity: 0.7; }
89
+ }
90
+ </style>
src/lib/components/Pages/Battle.svelte CHANGED
@@ -4,7 +4,9 @@
4
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
5
  import BattleField from '../Battle/BattleField.svelte';
6
  import BattleControls from '../Battle/BattleControls.svelte';
7
- import { BattleService } from '$lib/db/battleService';
 
 
8
  import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
9
 
10
  export let playerPiclet: PicletInstance;
@@ -13,19 +15,32 @@
13
  export let onBattleEnd: (result: any) => void = () => {};
14
  export let rosterPiclets: PicletInstance[] = []; // All roster piclets passed from parent
15
 
 
 
 
 
 
 
16
  // Battle state
17
  let currentMessage = isWildBattle
18
  ? `A wild ${enemyPiclet.nickname} appeared!`
19
  : `Trainer wants to battle!`;
20
- let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended' = 'intro';
21
  let processingTurn = false;
22
  let battleEnded = false;
23
 
24
- // HP animation states
25
  let playerHpPercentage = playerPiclet.currentHp / playerPiclet.maxHp;
26
  let enemyHpPercentage = enemyPiclet.currentHp / enemyPiclet.maxHp;
27
 
28
  onMount(() => {
 
 
 
 
 
 
 
29
  // Start intro sequence
30
  setTimeout(() => {
31
  currentMessage = `Go, ${playerPiclet.nickname}!`;
@@ -63,97 +78,123 @@
63
  }
64
 
65
  function handleMoveSelect(move: BattleMove) {
 
 
66
  battlePhase = 'main';
67
  processingTurn = true;
68
- currentMessage = `${playerPiclet.nickname} used ${move.name}!`;
69
 
70
- setTimeout(() => {
71
- if (!BattleService.doesMoveHit(move.accuracy)) {
72
- currentMessage = `${playerPiclet.nickname}'s attack missed!`;
73
- setTimeout(() => enemyTurn(), 1500);
 
 
 
 
 
 
 
 
 
 
 
74
  return;
75
  }
76
 
77
- // Calculate damage with type effectiveness
78
- const { damage, effectiveness } = BattleService.calculateDamage(playerPiclet, enemyPiclet, move);
 
 
 
 
79
 
80
- // Update enemy HP
81
- const newHp = Math.max(0, enemyPiclet.currentHp - damage);
82
- enemyPiclet.currentHp = newHp;
83
- enemyHpPercentage = newHp / enemyPiclet.maxHp;
84
 
85
- // Show effectiveness message if applicable
86
- const effectivenessMessage = getEffectivenessText(effectiveness);
87
- if (effectivenessMessage) {
88
- setTimeout(() => {
89
- currentMessage = effectivenessMessage;
90
- }, 1000);
 
 
 
 
 
 
 
 
 
 
91
  }
92
 
93
- setTimeout(() => {
94
- if (enemyHpPercentage <= 0) {
95
- currentMessage = `${enemyPiclet.nickname} fainted!`;
 
 
 
96
  battleEnded = true;
97
- setTimeout(() => onBattleEnd(true), 2000);
 
 
 
 
 
 
98
  } else {
99
- enemyTurn();
 
 
 
100
  }
101
- }, effectivenessMessage ? 2500 : 1500);
102
- }, 1500);
 
 
 
 
103
  }
104
 
105
- function enemyTurn() {
106
- const enemyMove = enemyPiclet.moves[Math.floor(Math.random() * enemyPiclet.moves.length)];
107
- currentMessage = `${enemyPiclet.nickname} used ${enemyMove.name}!`;
108
 
109
- setTimeout(() => {
110
- if (!BattleService.doesMoveHit(enemyMove.accuracy)) {
111
- currentMessage = `${enemyPiclet.nickname}'s attack missed!`;
112
- setTimeout(() => {
113
- currentMessage = `What will ${playerPiclet.nickname} do?`;
114
- processingTurn = false;
115
- }, 1500);
116
- return;
117
- }
118
-
119
- const { damage, effectiveness } = BattleService.calculateDamage(enemyPiclet, playerPiclet, enemyMove);
120
-
121
- // Update player HP
122
- const newHp = Math.max(0, playerPiclet.currentHp - damage);
123
- playerPiclet.currentHp = newHp;
124
- playerHpPercentage = newHp / playerPiclet.maxHp;
125
-
126
- // Show effectiveness message if applicable
127
- const effectivenessMessage = getEffectivenessText(effectiveness);
128
- if (effectivenessMessage) {
129
- setTimeout(() => {
130
- currentMessage = effectivenessMessage;
131
- }, 1000);
132
- }
133
-
134
- setTimeout(() => {
135
- if (playerHpPercentage <= 0) {
136
- currentMessage = `${playerPiclet.nickname} fainted!`;
137
- battleEnded = true;
138
- setTimeout(() => onBattleEnd(false), 2000);
139
- } else {
140
- currentMessage = `What will ${playerPiclet.nickname} do?`;
141
- processingTurn = false;
142
- }
143
- }, effectivenessMessage ? 2500 : 1500);
144
- }, 1500);
145
  }
146
 
147
  function handlePicletSelect(piclet: PicletInstance) {
 
 
148
  battlePhase = 'main';
149
- currentMessage = `Come back, ${playerPiclet.nickname}!`;
 
150
  setTimeout(() => {
151
- playerPiclet = piclet;
152
- playerHpPercentage = piclet.currentHp / piclet.maxHp;
153
- currentMessage = `Go, ${piclet.nickname}!`;
154
- setTimeout(() => {
155
- currentMessage = `What will ${piclet.nickname} do?`;
156
- }, 1500);
 
 
 
 
 
 
 
 
 
 
 
157
  }, 1500);
158
  }
159
 
@@ -173,11 +214,12 @@
173
 
174
  <div class="battle-content">
175
  <BattleField
176
- {playerPiclet}
177
- {enemyPiclet}
178
  {playerHpPercentage}
179
  {enemyHpPercentage}
180
  showIntro={battlePhase === 'intro'}
 
181
  />
182
 
183
  <BattleControls
@@ -186,9 +228,10 @@
186
  {processingTurn}
187
  {battleEnded}
188
  {isWildBattle}
189
- {playerPiclet}
190
- {enemyPiclet}
191
  {rosterPiclets}
 
192
  onAction={handleAction}
193
  onMoveSelect={handleMoveSelect}
194
  onPicletSelect={handlePicletSelect}
 
4
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
5
  import BattleField from '../Battle/BattleField.svelte';
6
  import BattleControls from '../Battle/BattleControls.svelte';
7
+ import { BattleEngine } from '$lib/battle-engine/BattleEngine';
8
+ import type { BattleState, MoveAction } from '$lib/battle-engine/types';
9
+ import { picletInstanceToBattleDefinition, battlePicletToInstance } from '$lib/utils/battleConversion';
10
  import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
11
 
12
  export let playerPiclet: PicletInstance;
 
15
  export let onBattleEnd: (result: any) => void = () => {};
16
  export let rosterPiclets: PicletInstance[] = []; // All roster piclets passed from parent
17
 
18
+ // Initialize battle engine
19
+ let battleEngine: BattleEngine;
20
+ let battleState: BattleState;
21
+ let currentPlayerPiclet = playerPiclet;
22
+ let currentEnemyPiclet = enemyPiclet;
23
+
24
  // Battle state
25
  let currentMessage = isWildBattle
26
  ? `A wild ${enemyPiclet.nickname} appeared!`
27
  : `Trainer wants to battle!`;
28
+ let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended' = 'intro';
29
  let processingTurn = false;
30
  let battleEnded = false;
31
 
32
+ // HP animation states
33
  let playerHpPercentage = playerPiclet.currentHp / playerPiclet.maxHp;
34
  let enemyHpPercentage = enemyPiclet.currentHp / enemyPiclet.maxHp;
35
 
36
  onMount(() => {
37
+ // Initialize battle engine with converted piclet definitions
38
+ const playerDefinition = picletInstanceToBattleDefinition(playerPiclet);
39
+ const enemyDefinition = picletInstanceToBattleDefinition(enemyPiclet);
40
+
41
+ battleEngine = new BattleEngine(playerDefinition, enemyDefinition, playerPiclet.level, enemyPiclet.level);
42
+ battleState = battleEngine.getState();
43
+
44
  // Start intro sequence
45
  setTimeout(() => {
46
  currentMessage = `Go, ${playerPiclet.nickname}!`;
 
78
  }
79
 
80
  function handleMoveSelect(move: BattleMove) {
81
+ if (!battleEngine) return;
82
+
83
  battlePhase = 'main';
84
  processingTurn = true;
 
85
 
86
+ // Find the corresponding move in the battle engine
87
+ const battleMove = battleState.playerPiclet.moves.find(m => m.move.name === move.name);
88
+ if (!battleMove) return;
89
+
90
+ const moveAction: MoveAction = {
91
+ type: 'move',
92
+ moveIndex: battleState.playerPiclet.moves.indexOf(battleMove)
93
+ };
94
+
95
+ try {
96
+ // Choose random enemy move (could be improved with AI)
97
+ const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0);
98
+ if (availableEnemyMoves.length === 0) {
99
+ currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`;
100
+ processingTurn = false;
101
  return;
102
  }
103
 
104
+ const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
105
+ const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
106
+ const enemyAction: MoveAction = {
107
+ type: 'move',
108
+ moveIndex: enemyMoveIndex
109
+ };
110
 
111
+ // Execute the turn - battle engine handles priority automatically
112
+ const result = battleEngine.executeTurn(moveAction, enemyAction);
113
+ battleState = battleEngine.getState();
 
114
 
115
+ // Show battle messages with timing
116
+ if (result.log && result.log.length > 0) {
117
+ let messageIndex = 0;
118
+ function showNextBattleMessage() {
119
+ if (messageIndex < result.log.length) {
120
+ currentMessage = result.log[messageIndex];
121
+ messageIndex++;
122
+ setTimeout(showNextBattleMessage, 1500);
123
+ } else {
124
+ // After all messages, check battle end or continue
125
+ finalizeTurn();
126
+ }
127
+ }
128
+ showNextBattleMessage();
129
+ } else {
130
+ finalizeTurn();
131
  }
132
 
133
+ function finalizeTurn() {
134
+ // Update UI state from battle engine
135
+ updateUIFromBattleState();
136
+
137
+ // Check for battle end
138
+ if (battleState.winner) {
139
  battleEnded = true;
140
+ const winMessage = battleState.winner === 'player'
141
+ ? `${currentEnemyPiclet.nickname} fainted! You won!`
142
+ : `${currentPlayerPiclet.nickname} fainted! You lost!`;
143
+ currentMessage = winMessage;
144
+ setTimeout(() => {
145
+ onBattleEnd(battleState.winner === 'player');
146
+ }, 2000);
147
  } else {
148
+ setTimeout(() => {
149
+ currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
150
+ processingTurn = false;
151
+ }, 1000);
152
  }
153
+ }
154
+ } catch (error) {
155
+ console.error('Battle engine error:', error);
156
+ currentMessage = 'Something went wrong in battle!';
157
+ processingTurn = false;
158
+ }
159
  }
160
 
161
+
162
+ function updateUIFromBattleState() {
163
+ if (!battleState) return;
164
 
165
+ // Update player piclet state
166
+ currentPlayerPiclet = battlePicletToInstance(battleState.playerPiclet, currentPlayerPiclet);
167
+ playerHpPercentage = battleState.playerPiclet.currentHp / battleState.playerPiclet.maxHp;
168
+
169
+ // Update enemy piclet state
170
+ currentEnemyPiclet = battlePicletToInstance(battleState.opponentPiclet, currentEnemyPiclet);
171
+ enemyHpPercentage = battleState.opponentPiclet.currentHp / battleState.opponentPiclet.maxHp;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  function handlePicletSelect(piclet: PicletInstance) {
175
+ if (!battleEngine) return;
176
+
177
  battlePhase = 'main';
178
+ currentMessage = `Come back, ${currentPlayerPiclet.nickname}!`;
179
+
180
  setTimeout(() => {
181
+ // Convert the selected piclet to battle definition and switch
182
+ const newPicletDefinition = picletInstanceToBattleDefinition(piclet);
183
+
184
+ try {
185
+ // TODO: Implement switching in battle engine
186
+ // For now, just update the UI
187
+ currentPlayerPiclet = piclet;
188
+ playerHpPercentage = piclet.currentHp / piclet.maxHp;
189
+ currentMessage = `Go, ${piclet.nickname}!`;
190
+
191
+ setTimeout(() => {
192
+ currentMessage = `What will ${piclet.nickname} do?`;
193
+ }, 1500);
194
+ } catch (error) {
195
+ console.error('Switch error:', error);
196
+ currentMessage = 'Unable to switch Piclets!';
197
+ }
198
  }, 1500);
199
  }
200
 
 
214
 
215
  <div class="battle-content">
216
  <BattleField
217
+ playerPiclet={currentPlayerPiclet}
218
+ enemyPiclet={currentEnemyPiclet}
219
  {playerHpPercentage}
220
  {enemyHpPercentage}
221
  showIntro={battlePhase === 'intro'}
222
+ {battleState}
223
  />
224
 
225
  <BattleControls
 
228
  {processingTurn}
229
  {battleEnded}
230
  {isWildBattle}
231
+ playerPiclet={currentPlayerPiclet}
232
+ enemyPiclet={currentEnemyPiclet}
233
  {rosterPiclets}
234
+ {battleState}
235
  onAction={handleAction}
236
  onMoveSelect={handleMoveSelect}
237
  onPicletSelect={handlePicletSelect}
src/lib/components/PicletGenerator/PicletGenerator.svelte CHANGED
@@ -1,5 +1,6 @@
1
  <script lang="ts">
2
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
 
3
  import type { PicletInstance } from '$lib/db/schema';
4
  import UploadStep from './UploadStep.svelte';
5
  import WorkflowProgress from './WorkflowProgress.svelte';
@@ -568,7 +569,11 @@ The output should be formatted as a JSON instance that conforms to the schema be
568
  "required": ["hp", "attack", "defense", "speed"],
569
  "additionalProperties": false
570
  },
571
- "nature": {"type": "string", "description": "Personality trait affecting behavior (e.g., 'bold', 'timid', 'hasty')"},
 
 
 
 
572
  "specialAbility": {
573
  "type": "object",
574
  "properties": {
 
1
  <script lang="ts">
2
  import type { PicletGeneratorProps, PicletWorkflowState, CaptionType, CaptionLength, PicletStats } from '$lib/types';
3
+ import { Nature } from '$lib/types';
4
  import type { PicletInstance } from '$lib/db/schema';
5
  import UploadStep from './UploadStep.svelte';
6
  import WorkflowProgress from './WorkflowProgress.svelte';
 
569
  "required": ["hp", "attack", "defense", "speed"],
570
  "additionalProperties": false
571
  },
572
+ "nature": {
573
+ "type": "string",
574
+ "enum": ["hardy", "docile", "serious", "bashful", "quirky", "lonely", "brave", "adamant", "naughty", "bold", "relaxed", "impish", "lax", "timid", "hasty", "jolly", "naive", "modest", "mild", "quiet", "gentle", "sassy", "careful", "calm", "reckless"],
575
+ "description": "Personality trait affecting behavior and battle style"
576
+ },
577
  "specialAbility": {
578
  "type": "object",
579
  "properties": {
src/lib/db/piclets.ts CHANGED
@@ -2,6 +2,7 @@ import { db } from './index';
2
  import type { PicletInstance, Monster, BattleMove } from './schema';
3
  import { PicletType, AttackType, getTypeFromConcept } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
 
5
 
6
  // Convert a generated Monster to a PicletInstance
7
  export async function monsterToPicletInstance(monster: Monster, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
@@ -9,13 +10,97 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
9
  throw new Error('Monster must have stats to create PicletInstance');
10
  }
11
 
12
- const stats = monster.stats as PicletStats;
 
13
 
14
- // Calculate base stats from battle-ready format
15
- const baseHp = Math.floor(stats.baseStats.hp * 2 + 50);
16
- const baseAttack = Math.floor(stats.baseStats.attack * 1.5 + 30);
17
- const baseDefense = Math.floor(stats.baseStats.defense * 1.5 + 30);
18
- const baseSpeed = Math.floor(stats.baseStats.speed * 1.5 + 30);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  // Field stats are variations of regular stats
21
  const baseFieldAttack = Math.floor(baseAttack * 0.8);
@@ -27,27 +112,6 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
27
 
28
  const maxHp = calculateHp(baseHp, level);
29
 
30
- // Determine primary type from battle stats
31
- const normalizedType = stats.primaryType.toLowerCase();
32
-
33
- // Validate that the type exists in our enum
34
- const validType = Object.values(PicletType).find(type => type === normalizedType);
35
- const primaryType = validType || getTypeFromConcept(monster.concept, monster.imageCaption);
36
-
37
- if (!validType) {
38
- console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
39
- }
40
-
41
- // Create moves from battle-ready format
42
- const moves: BattleMove[] = stats.movepool.map(move => ({
43
- name: move.name,
44
- type: move.type as unknown as AttackType,
45
- power: move.power,
46
- accuracy: move.accuracy,
47
- pp: move.pp,
48
- currentPp: move.pp,
49
- description: `${move.effects.length} effects` // Simplified description for now
50
- }));
51
 
52
  const bst = baseHp + baseAttack + baseDefense + baseFieldAttack + baseFieldDefense + baseSpeed;
53
 
@@ -79,7 +143,7 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
79
 
80
  // Battle
81
  moves,
82
- nature: 'hardy', // Default nature
83
 
84
  // Roster
85
  isInRoster: false,
@@ -88,7 +152,7 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
88
  // Metadata
89
  caughtAt: new Date(),
90
  bst,
91
- tier: stats.tier, // Use tier from battle-ready stats
92
  role: 'balanced', // Could be enhanced based on stat distribution
93
  variance: 0,
94
 
@@ -96,7 +160,7 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
96
  imageUrl: monster.imageUrl,
97
  imageData: monster.imageData,
98
  imageCaption: monster.imageCaption,
99
- concept: stats.description || monster.concept, // Use the Monster Lore description
100
  imagePrompt: monster.imagePrompt
101
  };
102
  }
 
2
  import type { PicletInstance, Monster, BattleMove } from './schema';
3
  import { PicletType, AttackType, getTypeFromConcept } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
5
+ import { Nature } from '../types';
6
 
7
  // Convert a generated Monster to a PicletInstance
8
  export async function monsterToPicletInstance(monster: Monster, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
 
10
  throw new Error('Monster must have stats to create PicletInstance');
11
  }
12
 
13
+ // Check if this is a new battle-ready monster or legacy format
14
+ const isBattleReady = 'baseStats' in monster.stats;
15
 
16
+ let baseHp: number, baseAttack: number, baseDefense: number, baseSpeed: number;
17
+ let moves: BattleMove[];
18
+ let primaryType: PicletType;
19
+ let tier: string;
20
+
21
+ if (isBattleReady) {
22
+ const stats = monster.stats as PicletStats;
23
+
24
+ // Calculate base stats from battle-ready format
25
+ baseHp = Math.floor(stats.baseStats.hp * 2 + 50);
26
+ baseAttack = Math.floor(stats.baseStats.attack * 1.5 + 30);
27
+ baseDefense = Math.floor(stats.baseStats.defense * 1.5 + 30);
28
+ baseSpeed = Math.floor(stats.baseStats.speed * 1.5 + 30);
29
+
30
+ // Determine primary type from battle stats
31
+ const normalizedType = stats.primaryType.toLowerCase();
32
+ const validType = Object.values(PicletType).find(type => type === normalizedType);
33
+ primaryType = validType || getTypeFromConcept(monster.concept, monster.imageCaption);
34
+
35
+ if (!validType) {
36
+ console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
37
+ }
38
+
39
+ // Create moves from battle-ready format
40
+ moves = stats.movepool.map(move => ({
41
+ name: move.name,
42
+ type: move.type as unknown as AttackType,
43
+ power: move.power,
44
+ accuracy: move.accuracy,
45
+ pp: move.pp,
46
+ currentPp: move.pp,
47
+ description: `${move.effects.length} effects` // Simplified description for now
48
+ }));
49
+
50
+ tier = stats.tier;
51
+ } else {
52
+ // Legacy format
53
+ const legacyStats = monster.stats as any;
54
+ baseHp = Math.floor(legacyStats.HP * 2 + 50);
55
+ baseAttack = Math.floor(legacyStats.attack * 1.5 + 30);
56
+ baseDefense = Math.floor(legacyStats.defence * 1.5 + 30);
57
+ baseSpeed = Math.floor(legacyStats.speed * 1.5 + 30);
58
+
59
+ // Auto-detect type for legacy monsters
60
+ primaryType = getTypeFromConcept(monster.concept, monster.imageCaption);
61
+
62
+ // Create legacy moves
63
+ moves = [
64
+ {
65
+ name: legacyStats.attackActionName,
66
+ type: primaryType as unknown as AttackType,
67
+ power: 50,
68
+ accuracy: 95,
69
+ pp: 20,
70
+ currentPp: 20,
71
+ description: legacyStats.attackActionDescription
72
+ },
73
+ {
74
+ name: legacyStats.buffActionName,
75
+ type: AttackType.NORMAL,
76
+ power: 0,
77
+ accuracy: 100,
78
+ pp: 15,
79
+ currentPp: 15,
80
+ description: legacyStats.buffActionDescription
81
+ },
82
+ {
83
+ name: legacyStats.debuffActionName,
84
+ type: AttackType.NORMAL,
85
+ power: 0,
86
+ accuracy: 85,
87
+ pp: 15,
88
+ currentPp: 15,
89
+ description: legacyStats.debuffActionDescription
90
+ },
91
+ {
92
+ name: legacyStats.specialActionName,
93
+ type: primaryType as unknown as AttackType,
94
+ power: 80,
95
+ accuracy: 90,
96
+ pp: 5,
97
+ currentPp: 5,
98
+ description: legacyStats.specialActionDescription
99
+ }
100
+ ];
101
+
102
+ tier = legacyStats.tier || 'medium';
103
+ }
104
 
105
  // Field stats are variations of regular stats
106
  const baseFieldAttack = Math.floor(baseAttack * 0.8);
 
112
 
113
  const maxHp = calculateHp(baseHp, level);
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  const bst = baseHp + baseAttack + baseDefense + baseFieldAttack + baseFieldDefense + baseSpeed;
117
 
 
143
 
144
  // Battle
145
  moves,
146
+ nature: Nature.HARDY, // Default nature
147
 
148
  // Roster
149
  isInRoster: false,
 
152
  // Metadata
153
  caughtAt: new Date(),
154
  bst,
155
+ tier: tier, // Use tier from stats
156
  role: 'balanced', // Could be enhanced based on stat distribution
157
  variance: 0,
158
 
 
160
  imageUrl: monster.imageUrl,
161
  imageData: monster.imageData,
162
  imageCaption: monster.imageCaption,
163
+ concept: monster.concept, // Use the original concept
164
  imagePrompt: monster.imagePrompt
165
  };
166
  }
src/lib/types/index.ts CHANGED
@@ -104,6 +104,44 @@ export interface PicletGeneratorProps {
104
  }
105
 
106
  // Piclet Stats Types - now compatible with battle engine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  export interface PicletStats {
108
  name: string;
109
  description: string;
@@ -116,7 +154,7 @@ export interface PicletStats {
116
  defense: number;
117
  speed: number;
118
  };
119
- nature: string;
120
  specialAbility: {
121
  name: string;
122
  description: string;
 
104
  }
105
 
106
  // Piclet Stats Types - now compatible with battle engine
107
+ // Nature enum defining personality traits that affect Piclet behavior
108
+ export enum Nature {
109
+ // Balanced natures (no stat modification)
110
+ HARDY = 'hardy',
111
+ DOCILE = 'docile',
112
+ SERIOUS = 'serious',
113
+ BASHFUL = 'bashful',
114
+ QUIRKY = 'quirky',
115
+
116
+ // Attack-focused natures
117
+ LONELY = 'lonely', // +Attack, -Defense
118
+ BRAVE = 'brave', // +Attack, -Speed
119
+ ADAMANT = 'adamant', // +Attack, -Special Attack (represented as general attack boost)
120
+ NAUGHTY = 'naughty', // +Attack, -Special Defense (represented as general attack boost)
121
+
122
+ // Defense-focused natures
123
+ BOLD = 'bold', // +Defense, -Attack
124
+ RELAXED = 'relaxed', // +Defense, -Speed
125
+ IMPISH = 'impish', // +Defense, -Special Attack
126
+ LAX = 'lax', // +Defense, -Special Defense
127
+
128
+ // Speed-focused natures
129
+ TIMID = 'timid', // +Speed, -Attack
130
+ HASTY = 'hasty', // +Speed, -Defense
131
+ JOLLY = 'jolly', // +Speed, -Special Attack
132
+ NAIVE = 'naive', // +Speed, -Special Defense
133
+
134
+ // Special natures (creative/tactical)
135
+ MODEST = 'modest', // Tactical, prefers status moves
136
+ MILD = 'mild', // Gentle but determined
137
+ QUIET = 'quiet', // Observant, defensive style
138
+ GENTLE = 'gentle', // Supportive, healing-focused
139
+ SASSY = 'sassy', // Unpredictable, varied moves
140
+ CAREFUL = 'careful', // Cautious, defensive
141
+ CALM = 'calm', // Composed, balanced approach
142
+ RECKLESS = 'reckless' // High-risk, high-reward style
143
+ }
144
+
145
  export interface PicletStats {
146
  name: string;
147
  description: string;
 
154
  defense: number;
155
  speed: number;
156
  };
157
+ nature: Nature;
158
  specialAbility: {
159
  name: string;
160
  description: string;
src/lib/utils/battleConversion.ts ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Conversion utilities for transforming database types to battle engine types
3
+ */
4
+
5
+ import type { PicletInstance, BattleMove } from '$lib/db/schema';
6
+ import type { PicletDefinition, Move, BaseStats, SpecialAbility } from '$lib/battle-engine/types';
7
+ import type { PicletStats, BattleEffect, AbilityTrigger } from '$lib/types';
8
+ import { PicletType, AttackType } from '$lib/types/picletTypes';
9
+
10
+ /**
11
+ * Convert PicletInstance to PicletDefinition for battle engine use
12
+ */
13
+ export function picletInstanceToBattleDefinition(instance: PicletInstance): PicletDefinition {
14
+ // Convert base stats from instance level to definition level
15
+ const baseStats: BaseStats = {
16
+ hp: Math.floor(instance.baseHp / 2 - 25), // Reverse the conversion from monsterToPicletInstance
17
+ attack: Math.floor((instance.baseAttack - 30) / 1.5),
18
+ defense: Math.floor((instance.baseDefense - 30) / 1.5),
19
+ speed: Math.floor((instance.baseSpeed - 30) / 1.5)
20
+ };
21
+
22
+ // Convert simple moves to full battle moves
23
+ const movepool: Move[] = instance.moves.map(move => convertBattleMoveToMove(move, instance.primaryType));
24
+
25
+ // Create a basic special ability if none exists
26
+ const specialAbility: SpecialAbility = {
27
+ name: "Adaptive",
28
+ description: "A basic ability that adapts to combat situations",
29
+ triggers: [{
30
+ event: "endOfTurn",
31
+ effects: [{
32
+ type: 'heal',
33
+ target: 'self',
34
+ amount: 'small',
35
+ condition: 'ifLowHp'
36
+ }]
37
+ }]
38
+ };
39
+
40
+ // Determine tier based on BST (Base Stat Total)
41
+ const bst = baseStats.hp + baseStats.attack + baseStats.defense + baseStats.speed;
42
+ let tier: 'low' | 'medium' | 'high' | 'legendary';
43
+ if (bst <= 300) tier = 'low';
44
+ else if (bst <= 400) tier = 'medium';
45
+ else if (bst <= 500) tier = 'high';
46
+ else tier = 'legendary';
47
+
48
+ return {
49
+ name: instance.nickname || instance.typeId,
50
+ description: instance.concept,
51
+ tier,
52
+ primaryType: convertPicletTypeToType(instance.primaryType),
53
+ secondaryType: instance.secondaryType ? convertPicletTypeToType(instance.secondaryType) : undefined,
54
+ baseStats,
55
+ nature: instance.nature,
56
+ specialAbility,
57
+ movepool
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Convert BattleMove to full Move for battle engine
63
+ */
64
+ function convertBattleMoveToMove(battleMove: BattleMove, primaryType: PicletType): Move {
65
+ // Create basic effects based on move properties
66
+ const effects: any[] = [];
67
+
68
+ if (battleMove.power > 0) {
69
+ effects.push({
70
+ type: 'damage',
71
+ target: 'opponent',
72
+ amount: battleMove.power >= 80 ? 'strong' :
73
+ battleMove.power >= 60 ? 'normal' : 'weak'
74
+ });
75
+ }
76
+
77
+ // Add status or stat effects for non-damaging moves
78
+ if (battleMove.power === 0) {
79
+ effects.push({
80
+ type: 'modifyStats',
81
+ target: 'self',
82
+ stats: { attack: 'increase' }
83
+ });
84
+ }
85
+
86
+ return {
87
+ name: battleMove.name,
88
+ type: convertAttackTypeToType(battleMove.type),
89
+ power: battleMove.power,
90
+ accuracy: battleMove.accuracy,
91
+ pp: battleMove.pp,
92
+ priority: 0,
93
+ flags: battleMove.power > 0 && battleMove.name.toLowerCase().includes('tackle') ? ['contact'] : [],
94
+ effects
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Convert PicletType to battle engine type
100
+ */
101
+ function convertPicletTypeToType(picletType: PicletType): 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine' {
102
+ const typeMap: Record<PicletType, any> = {
103
+ [PicletType.BEAST]: 'beast',
104
+ [PicletType.BUG]: 'bug',
105
+ [PicletType.AQUATIC]: 'aquatic',
106
+ [PicletType.FLORA]: 'flora',
107
+ [PicletType.MINERAL]: 'mineral',
108
+ [PicletType.SPACE]: 'space',
109
+ [PicletType.MACHINA]: 'machina',
110
+ [PicletType.STRUCTURE]: 'structure',
111
+ [PicletType.CULTURE]: 'culture',
112
+ [PicletType.CUISINE]: 'cuisine'
113
+ };
114
+
115
+ return typeMap[picletType] || 'beast';
116
+ }
117
+
118
+ /**
119
+ * Convert AttackType to battle engine type
120
+ */
121
+ function convertAttackTypeToType(attackType: AttackType): 'beast' | 'bug' | 'aquatic' | 'flora' | 'mineral' | 'space' | 'machina' | 'structure' | 'culture' | 'cuisine' | 'normal' {
122
+ // AttackType and PicletType should align, but handle the NORMAL case
123
+ if (attackType === AttackType.NORMAL) {
124
+ return 'normal';
125
+ }
126
+
127
+ // Map other attack types to piclet types
128
+ const typeMap: Record<string, any> = {
129
+ 'beast': 'beast',
130
+ 'bug': 'bug',
131
+ 'aquatic': 'aquatic',
132
+ 'flora': 'flora',
133
+ 'mineral': 'mineral',
134
+ 'space': 'space',
135
+ 'machina': 'machina',
136
+ 'structure': 'structure',
137
+ 'culture': 'culture',
138
+ 'cuisine': 'cuisine'
139
+ };
140
+
141
+ return typeMap[attackType.toString()] || 'normal';
142
+ }
143
+
144
+ /**
145
+ * Convert battle engine BattlePiclet back to PicletInstance for state updates
146
+ */
147
+ export function battlePicletToInstance(battlePiclet: any, originalInstance: PicletInstance): PicletInstance {
148
+ return {
149
+ ...originalInstance,
150
+ currentHp: battlePiclet.currentHp,
151
+ maxHp: battlePiclet.maxHp,
152
+ level: battlePiclet.level,
153
+ attack: battlePiclet.attack,
154
+ defense: battlePiclet.defense,
155
+ speed: battlePiclet.speed,
156
+ // Update moves with current PP
157
+ moves: battlePiclet.moves.map((moveData: any, index: number) => ({
158
+ ...originalInstance.moves[index],
159
+ currentPp: moveData.currentPP
160
+ }))
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Convert PicletStats (from generation) to PicletDefinition for battle engine
166
+ */
167
+ export function picletStatsToBattleDefinition(stats: PicletStats, name: string, concept: string): PicletDefinition {
168
+ return {
169
+ name: stats.name || name,
170
+ description: stats.description || concept,
171
+ tier: stats.tier,
172
+ primaryType: stats.primaryType,
173
+ secondaryType: stats.secondaryType || undefined,
174
+ baseStats: stats.baseStats,
175
+ nature: stats.nature,
176
+ specialAbility: stats.specialAbility,
177
+ movepool: stats.movepool
178
+ };
179
+ }