File size: 13,104 Bytes
c703ea3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
<script lang="ts">
  import type { PicletInstance } from '$lib/db/schema';
  import type { GradioClient } from '$lib/types';

  interface BattleUpdate {
    battle_updates: string[];
    player_pokemon_status: string;
    player_pokemon_hp: 'Empty' | 'Very Low' | 'Low' | 'Medium' | 'High' | 'Very High' | 'Full';
    enemy_pokemon_status: string;
    enemy_pokemon_hp: 'Empty' | 'Very Low' | 'Low' | 'Medium' | 'High' | 'Very High' | 'Full';
    next_to_act: 'player' | 'enemy';
    available_actions: string[];
  }

  interface Props {
    playerPiclet: PicletInstance;
    enemyPiclet: PicletInstance;
    commandClient: GradioClient;
    onBattleEnd: (winner: 'player' | 'enemy') => void;
    rosterPiclets?: PicletInstance[]; // Optional roster for switching
  }

  let { playerPiclet, enemyPiclet, commandClient, onBattleEnd, rosterPiclets }: Props = $props();

  // Battle state
  let battleState: BattleUpdate = $state({
    battle_updates: [],
    player_pokemon_status: 'Ready for battle',
    player_pokemon_hp: 'Full',
    enemy_pokemon_status: 'Ready for battle', 
    enemy_pokemon_hp: 'Full',
    next_to_act: 'player',
    available_actions: []
  });

  let battleHistory: string[] = $state([]);
  let isProcessing: boolean = $state(false);
  let currentPlayerPiclet: PicletInstance = $state(playerPiclet);
  let showPicletSelector: boolean = $state(false);

  // Dice rolling system
  function rollDice(): number {
    return Math.floor(Math.random() * 20) + 1; // D20 roll
  }

  function getActionEffectiveness(roll: number): { success: string; description: string } {
    if (roll === 20) return { success: 'Critical Success', description: 'The action succeeds spectacularly!' };
    if (roll >= 15) return { success: 'Success', description: 'The action succeeds well!' };
    if (roll >= 10) return { success: 'Partial Success', description: 'The action has some effect.' };
    if (roll >= 5) return { success: 'Failure', description: 'The action fails but something minor happens.' };
    if (roll === 1) return { success: 'Critical Failure', description: 'The action backfires!' };
    return { success: 'Failure', description: 'The action fails.' };
  }

  // Generate text using Command client
  async function generateBattleUpdate(prompt: string): Promise<BattleUpdate> {
    const message = {
      text: prompt,
      files: []
    };

    const result = await commandClient.predict("/chat", {
      message: message,
      max_new_tokens: 1000
    });

    const responseText = result.data || '';
    console.log('LLM Response:', responseText);

    // Extract JSON from response
    try {
      const jsonMatch = responseText.match(/\{[\s\S]*\}/);
      if (!jsonMatch) throw new Error('No JSON found in response');
      
      const battleUpdate: BattleUpdate = JSON.parse(jsonMatch[0]);
      return battleUpdate;
    } catch (error) {
      console.error('Failed to parse battle response:', error);
      // Fallback response
      return {
        battle_updates: ['The battle continues...'],
        player_pokemon_status: battleState.player_pokemon_status,
        player_pokemon_hp: battleState.player_pokemon_hp,
        enemy_pokemon_status: battleState.enemy_pokemon_status,
        enemy_pokemon_hp: battleState.enemy_pokemon_hp,
        next_to_act: battleState.next_to_act === 'player' ? 'enemy' : 'player',
        available_actions: ['Attack', 'Defend', 'Special Move']
      };
    }
  }

  // Initialize battle
  async function startBattle() {
    isProcessing = true;
    
    const initialPrompt = `Let's role play a Pokemon game using my custom Pokemon, the player will use ${currentPlayerPiclet.typeId} and the enemy will use ${enemyPiclet.typeId}.
You will return a brief description on what happens because of the action.
I will send you an update of the enemy or players move and the success of the action (in a DnD style). Be sure to be as creative and engaging as possible when defining battle updates and available actions.

Player Pokemon: ${currentPlayerPiclet.typeId}
${currentPlayerPiclet.description}

Enemy Pokemon: ${enemyPiclet.typeId}
${enemyPiclet.description}

Each response should be a json object with fields:
\`\`\`json
{
    "battle_updates": [list with 1 sentence per entry describing what just happened in battle],
    "player_pokemon_status": "1 sentence description of how the Pokemon is doing",
    "player_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
    "enemy_pokemon_status": "1 sentence description of how the Pokemon is doing",
    "enemy_pokemon_hp": "enum Empty, Very Low, Low, Medium, High, Very High, Full",
    "next_to_act": "enum player/enemy",
    "available_actions": ["short list of 1 sentence actions of what to have the next_to_act Pokemon do next"]
}
\`\`\`
Start with just some intro updates describing both monsters being on the battlefield.`;

    try {
      const update = await generateBattleUpdate(initialPrompt);
      battleState = update;
      battleHistory.push(initialPrompt);
    } catch (error) {
      console.error('Failed to start battle:', error);
    }
    
    isProcessing = false;
  }

  // Execute player action
  async function executeAction(actionDescription: string) {
    if (isProcessing) return;
    
    isProcessing = true;
    
    const roll = rollDice();
    const effectiveness = getActionEffectiveness(roll);
    
    const prompt = `Player chooses: "${actionDescription}"
Dice roll: ${roll}/20 (${effectiveness.success})
Effect: ${effectiveness.description}

Update the battle state based on this action and its effectiveness. Then have the enemy take their turn if appropriate.`;

    try {
      const update = await generateBattleUpdate(prompt);
      battleState = update;
      battleHistory.push(prompt);
      
      // Check for battle end conditions
      if (battleState.player_pokemon_hp === 'Empty') {
        onBattleEnd('enemy');
      } else if (battleState.enemy_pokemon_hp === 'Empty') {
        onBattleEnd('player');
      }
    } catch (error) {
      console.error('Failed to execute action:', error);
    }
    
    isProcessing = false;
  }
  
  // Switch Piclet function
  async function switchPiclet(newPiclet: PicletInstance) {
    if (isProcessing) return;
    
    isProcessing = true;
    showPicletSelector = false;
    
    const switchPrompt = `Player switches from ${currentPlayerPiclet.typeId} to ${newPiclet.typeId}!
    
New Pokemon: ${newPiclet.typeId}
${newPiclet.description}

Update the battle to show the switch and have the enemy react accordingly.`;
    
    try {
      currentPlayerPiclet = newPiclet;
      const update = await generateBattleUpdate(switchPrompt);
      battleState = update;
      battleHistory.push(switchPrompt);
    } catch (error) {
      console.error('Failed to switch Piclet:', error);
    }
    
    isProcessing = false;
  }

  // Auto-start battle when component mounts
  $effect(() => {
    startBattle();
  });

  // Export functions for parent component
  export { executeAction, switchPiclet };
</script>

<div class="llm-battle-engine">
  <!-- Battle Narrative Display -->
  <div class="battle-narrative">
    <h3>Battle Progress</h3>
    {#each battleState.battle_updates as update}
      <div class="battle-update">{update}</div>
    {/each}
  </div>
  
  <!-- Pokemon Status -->
  <div class="pokemon-status">
    <div class="player-status">
      <h4>{currentPlayerPiclet.typeId}</h4>
      <div class="hp-indicator hp-{battleState.player_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.player_pokemon_hp}</div>
      <p>{battleState.player_pokemon_status}</p>
    </div>
    
    <div class="enemy-status">
      <h4>{enemyPiclet.typeId}</h4>
      <div class="hp-indicator hp-{battleState.enemy_pokemon_hp.toLowerCase().replace(' ', '-')}">{battleState.enemy_pokemon_hp}</div>
      <p>{battleState.enemy_pokemon_status}</p>
    </div>
  </div>
  
  <!-- Available Actions (when it's player's turn) -->
  {#if battleState.next_to_act === 'player' && !isProcessing}
    <div class="available-actions">
      <h4>Choose Your Action:</h4>
      
      <!-- Battle Actions -->
      {#each battleState.available_actions as action}
        <button 
          class="action-button" 
          onclick={() => executeAction(action)}
        >
          {action}
        </button>
      {/each}
      
      <!-- Piclet Switching -->
      {#if rosterPiclets && rosterPiclets.length > 1}
        <button 
          class="switch-button" 
          onclick={() => showPicletSelector = !showPicletSelector}
        >
          🔄 Switch Piclet
        </button>
      {/if}
    </div>
    
    <!-- Piclet Selector -->
    {#if showPicletSelector && rosterPiclets}
      <div class="piclet-selector">
        <h4>Choose Piclet:</h4>
        <div class="piclet-grid">
          {#each rosterPiclets as piclet}
            {#if piclet.id !== currentPlayerPiclet.id}
              <button 
                class="piclet-option" 
                onclick={() => switchPiclet(piclet)}
              >
                <img src={piclet.imageUrl} alt={piclet.typeId} />
                <span>{piclet.typeId}</span>
                <span class="tier tier-{piclet.tier}">{piclet.tier}</span>
              </button>
            {/if}
          {/each}
        </div>
      </div>
    {/if}
  {:else if isProcessing}
    <div class="processing">
      <div class="spinner"></div>
      <p>Processing battle turn...</p>
    </div>
  {:else}
    <div class="enemy-turn">
      <p>Enemy is deciding their move...</p>
    </div>
  {/if}
</div>

<style>
  .llm-battle-engine {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1rem;
  }
  
  .battle-narrative {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 1rem;
    max-height: 200px;
    overflow-y: auto;
  }
  
  .battle-update {
    margin-bottom: 0.5rem;
    padding: 0.5rem;
    background: white;
    border-radius: 4px;
    border-left: 3px solid #007bff;
  }
  
  .pokemon-status {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }
  
  .player-status, .enemy-status {
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
  }
  
  .player-status {
    background: rgba(0, 123, 255, 0.1);
    border: 2px solid #007bff;
  }
  
  .enemy-status {
    background: rgba(220, 53, 69, 0.1);
    border: 2px solid #dc3545;
  }
  
  .hp-indicator {
    font-weight: bold;
    padding: 0.25rem 0.5rem;
    border-radius: 16px;
    margin: 0.5rem 0;
    display: inline-block;
  }
  
  .hp-full { background: #28a745; color: white; }
  .hp-very-high { background: #40c757; color: white; }
  .hp-high { background: #6bc267; color: white; }
  .hp-medium { background: #ffc107; color: black; }
  .hp-low { background: #fd7e14; color: white; }
  .hp-very-low { background: #dc3545; color: white; }
  .hp-empty { background: #6c757d; color: white; }
  
  .available-actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }
  
  .action-button {
    padding: 0.75rem 1rem;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1rem;
    transition: background-color 0.2s;
  }
  
  .action-button:hover {
    background: #0056b3;
  }
  
  .switch-button {
    padding: 0.75rem 1rem;
    background: #28a745;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 1rem;
    transition: background-color 0.2s;
    margin-top: 0.5rem;
  }
  
  .switch-button:hover {
    background: #1e7e34;
  }
  
  .piclet-selector {
    background: #f8f9fa;
    border-radius: 8px;
    padding: 1rem;
    margin-top: 1rem;
  }
  
  .piclet-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 0.5rem;
    margin-top: 0.5rem;
  }
  
  .piclet-option {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem;
    background: white;
    border: 2px solid #dee2e6;
    border-radius: 8px;
    cursor: pointer;
    transition: all 0.2s;
  }
  
  .piclet-option:hover {
    border-color: #007bff;
    background: #f0f7ff;
  }
  
  .piclet-option img {
    width: 40px;
    height: 40px;
    object-fit: cover;
    border-radius: 4px;
  }
  
  .piclet-option span {
    font-size: 0.8rem;
    text-align: center;
  }
  
  .tier {
    padding: 0.1rem 0.3rem;
    border-radius: 8px;
    font-size: 0.7rem;
    font-weight: bold;
    text-transform: uppercase;
  }
  
  .tier-low { background: #6c757d; color: white; }
  .tier-medium { background: #28a745; color: white; }
  .tier-high { background: #fd7e14; color: white; }
  .tier-legendary { background: #dc3545; color: white; }
  
  .processing, .enemy-turn {
    text-align: center;
    padding: 2rem;
  }
  
  .spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #f3f3f3;
    border-top: 4px solid #007bff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
    margin: 0 auto 1rem;
  }
  
  @keyframes spin {
    to { transform: rotate(360deg); }
  }
</style>