| <script lang="ts"> |
| import { onMount } from 'svelte'; |
| import { fade } from 'svelte/transition'; |
| import type { PicletInstance, BattleMove } from '$lib/db/schema'; |
| import BattleField from '../Battle/BattleField.svelte'; |
| import BattleControls from '../Battle/BattleControls.svelte'; |
| import { BattleEngine } from '$lib/battle-engine/BattleEngine'; |
| import type { BattleState, MoveAction } from '$lib/battle-engine/types'; |
| import { picletInstanceToBattleDefinition, battlePicletToInstance } from '$lib/utils/battleConversion'; |
| import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes'; |
| |
| export let playerPiclet: PicletInstance; |
| export let enemyPiclet: PicletInstance; |
| export let isWildBattle: boolean = true; |
| export let onBattleEnd: (result: any) => void = () => {}; |
| export let rosterPiclets: PicletInstance[] = []; |
| |
| |
| let battleEngine: BattleEngine; |
| let battleState: BattleState; |
| let currentPlayerPiclet = playerPiclet; |
| let currentEnemyPiclet = enemyPiclet; |
| |
| |
| let currentMessage = isWildBattle |
| ? `A wild ${enemyPiclet.nickname} appeared!` |
| : `Trainer wants to battle!`; |
| let battlePhase: 'intro' | 'main' | 'moveSelect' | 'picletSelect' | 'ended' = 'intro'; |
| let processingTurn = false; |
| let battleEnded = false; |
| |
| |
| let playerHpPercentage = playerPiclet.currentHp / playerPiclet.maxHp; |
| let enemyHpPercentage = enemyPiclet.currentHp / enemyPiclet.maxHp; |
| |
| onMount(() => { |
| |
| const playerDefinition = picletInstanceToBattleDefinition(playerPiclet); |
| const enemyDefinition = picletInstanceToBattleDefinition(enemyPiclet); |
| |
| battleEngine = new BattleEngine(playerDefinition, enemyDefinition, playerPiclet.level, enemyPiclet.level); |
| battleState = battleEngine.getState(); |
| |
| |
| setTimeout(() => { |
| currentMessage = `Go, ${playerPiclet.nickname}!`; |
| setTimeout(() => { |
| currentMessage = `What will ${playerPiclet.nickname} do?`; |
| battlePhase = 'main'; |
| }, 1500); |
| }, 2000); |
| }); |
| |
| function handleAction(action: string) { |
| if (processingTurn || battleEnded) return; |
| |
| switch (action) { |
| case 'catch': |
| if (isWildBattle) { |
| processingTurn = true; |
| currentMessage = 'You threw a Piclet Ball!'; |
| setTimeout(() => { |
| currentMessage = 'The wild piclet broke free!'; |
| processingTurn = false; |
| }, 2000); |
| } |
| break; |
| case 'run': |
| if (isWildBattle) { |
| currentMessage = 'Got away safely!'; |
| battleEnded = true; |
| setTimeout(() => onBattleEnd(false), 1500); |
| } else { |
| currentMessage = "You can't run from a trainer battle!"; |
| } |
| break; |
| } |
| } |
| |
| function handleMoveSelect(move: BattleMove) { |
| if (!battleEngine) return; |
| |
| battlePhase = 'main'; |
| processingTurn = true; |
| |
| |
| const battleMove = battleState.playerPiclet.moves.find(m => m.move.name === move.name); |
| if (!battleMove) return; |
| |
| const moveAction: MoveAction = { |
| type: 'move', |
| moveIndex: battleState.playerPiclet.moves.indexOf(battleMove) |
| }; |
| |
| try { |
| |
| const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0); |
| if (availableEnemyMoves.length === 0) { |
| currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`; |
| processingTurn = false; |
| return; |
| } |
| |
| const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)]; |
| const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove); |
| const enemyAction: MoveAction = { |
| type: 'move', |
| moveIndex: enemyMoveIndex |
| }; |
| |
| |
| const result = battleEngine.executeTurn(moveAction, enemyAction); |
| battleState = battleEngine.getState(); |
| |
| |
| if (result.log && result.log.length > 0) { |
| let messageIndex = 0; |
| function showNextBattleMessage() { |
| if (messageIndex < result.log.length) { |
| currentMessage = result.log[messageIndex]; |
| messageIndex++; |
| setTimeout(showNextBattleMessage, 1500); |
| } else { |
| |
| finalizeTurn(); |
| } |
| } |
| showNextBattleMessage(); |
| } else { |
| finalizeTurn(); |
| } |
| |
| function finalizeTurn() { |
| |
| updateUIFromBattleState(); |
| |
| |
| if (battleState.winner) { |
| battleEnded = true; |
| const winMessage = battleState.winner === 'player' |
| ? `${currentEnemyPiclet.nickname} fainted! You won!` |
| : `${currentPlayerPiclet.nickname} fainted! You lost!`; |
| currentMessage = winMessage; |
| setTimeout(() => { |
| onBattleEnd(battleState.winner === 'player'); |
| }, 2000); |
| } else { |
| setTimeout(() => { |
| currentMessage = `What will ${currentPlayerPiclet.nickname} do?`; |
| processingTurn = false; |
| }, 1000); |
| } |
| } |
| } catch (error) { |
| console.error('Battle engine error:', error); |
| currentMessage = 'Something went wrong in battle!'; |
| processingTurn = false; |
| } |
| } |
| |
| |
| function updateUIFromBattleState() { |
| if (!battleState) return; |
| |
| |
| currentPlayerPiclet = battlePicletToInstance(battleState.playerPiclet, currentPlayerPiclet); |
| playerHpPercentage = battleState.playerPiclet.currentHp / battleState.playerPiclet.maxHp; |
| |
| |
| currentEnemyPiclet = battlePicletToInstance(battleState.opponentPiclet, currentEnemyPiclet); |
| enemyHpPercentage = battleState.opponentPiclet.currentHp / battleState.opponentPiclet.maxHp; |
| } |
| |
| function handlePicletSelect(piclet: PicletInstance) { |
| if (!battleEngine) return; |
| |
| battlePhase = 'main'; |
| currentMessage = `Come back, ${currentPlayerPiclet.nickname}!`; |
| |
| setTimeout(() => { |
| |
| const newPicletDefinition = picletInstanceToBattleDefinition(piclet); |
| |
| try { |
| |
| |
| currentPlayerPiclet = piclet; |
| playerHpPercentage = piclet.currentHp / piclet.maxHp; |
| currentMessage = `Go, ${piclet.nickname}!`; |
| |
| setTimeout(() => { |
| currentMessage = `What will ${piclet.nickname} do?`; |
| }, 1500); |
| } catch (error) { |
| console.error('Switch error:', error); |
| currentMessage = 'Unable to switch Piclets!'; |
| } |
| }, 1500); |
| } |
| |
| function handleBack() { |
| battlePhase = 'main'; |
| } |
| </script> |
|
|
| <div class="battle-page" transition:fade={{ duration: 300 }}> |
| <nav class="battle-nav"> |
| <button class="back-button" on:click={() => onBattleEnd('cancelled')} style="display: none;"> |
| ← Back |
| </button> |
| <h1>{isWildBattle ? 'Wild Battle' : 'Battle'}</h1> |
| <div class="nav-spacer"></div> |
| </nav> |
| |
| <div class="battle-content"> |
| <BattleField |
| playerPiclet={currentPlayerPiclet} |
| enemyPiclet={currentEnemyPiclet} |
| {playerHpPercentage} |
| {enemyHpPercentage} |
| showIntro={battlePhase === 'intro'} |
| {battleState} |
| /> |
| |
| <BattleControls |
| {currentMessage} |
| {battlePhase} |
| {processingTurn} |
| {battleEnded} |
| {isWildBattle} |
| playerPiclet={currentPlayerPiclet} |
| enemyPiclet={currentEnemyPiclet} |
| {rosterPiclets} |
| {battleState} |
| onAction={handleAction} |
| onMoveSelect={handleMoveSelect} |
| onPicletSelect={handlePicletSelect} |
| onBack={handleBack} |
| /> |
| </div> |
| </div> |
|
|
| <style> |
| .battle-page { |
| position: fixed; |
| inset: 0; |
| z-index: 1000; |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| background: #f8f9fa; |
| overflow: hidden; |
| padding-top: env(safe-area-inset-top); |
| } |
| |
| @media (max-width: 768px) { |
| .battle-page { |
| background: white; |
| } |
| |
| .battle-page::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: env(safe-area-inset-top); |
| background: white; |
| z-index: 1; |
| } |
| } |
| |
| .battle-nav { |
| display: none; |
| } |
| |
| .back-button { |
| background: none; |
| border: none; |
| color: #007bff; |
| font-size: 1rem; |
| cursor: pointer; |
| padding: 0.5rem; |
| } |
| |
| .battle-nav h1 { |
| margin: 0; |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: #1a1a1a; |
| position: absolute; |
| left: 50%; |
| transform: translateX(-50%); |
| } |
| |
| .nav-spacer { |
| width: 60px; |
| } |
| |
| .battle-content { |
| flex: 1; |
| display: flex; |
| flex-direction: column; |
| overflow: hidden; |
| position: relative; |
| background: #f8f9fa; |
| } |
| </style> |