|
<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> |