|
<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; |
|
|
|
|
|
let playerEffects: Array<{type: string, emoji: string, duration: number}> = []; |
|
let enemyEffects: Array<{type: string, emoji: string, duration: number}> = []; |
|
let playerFlash = false; |
|
let enemyFlash = false; |
|
|
|
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 logBefore = battleEngine.getLog(); |
|
|
|
|
|
battleEngine.executeActions(moveAction, enemyAction); |
|
battleState = battleEngine.getState(); |
|
|
|
|
|
const logAfter = battleEngine.getLog(); |
|
const newLogEntries = logAfter.slice(logBefore.length); |
|
const result = { log: newLogEntries }; |
|
|
|
|
|
if (result.log && result.log.length > 0) { |
|
let messageIndex = 0; |
|
function showNextBattleMessage() { |
|
if (messageIndex < result.log.length) { |
|
const message = result.log[messageIndex]; |
|
currentMessage = message; |
|
|
|
|
|
triggerVisualEffectsFromMessage(message); |
|
|
|
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 triggerVisualEffectsFromMessage(message: string) { |
|
const playerName = currentPlayerPiclet.nickname; |
|
const enemyName = currentEnemyPiclet.nickname; |
|
|
|
|
|
if (message.includes('took') && message.includes('damage')) { |
|
if (message.includes(playerName)) { |
|
triggerDamageFlash('player'); |
|
} else if (message.includes(enemyName)) { |
|
triggerDamageFlash('enemy'); |
|
} |
|
} |
|
|
|
|
|
if (message.includes('critical hit')) { |
|
triggerEffect('both', 'critical', '💥', 1000); |
|
} |
|
|
|
|
|
if (message.includes("It's super effective")) { |
|
triggerEffect('both', 'superEffective', '⚡', 800); |
|
} else if (message.includes("not very effective")) { |
|
triggerEffect('both', 'notVeryEffective', '💨', 800); |
|
} |
|
|
|
|
|
if (message.includes('was burned')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'burn', '🔥', 1200); |
|
} else if (message.includes('was poisoned')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'poison', '☠️', 1200); |
|
} else if (message.includes('was paralyzed')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'paralyze', '⚡', 1200); |
|
} else if (message.includes('fell asleep')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'sleep', '😴', 1200); |
|
} else if (message.includes('was frozen')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'freeze', '❄️', 1200); |
|
} |
|
|
|
|
|
if (message.includes("'s") && (message.includes('rose') || message.includes('fell'))) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
const isIncrease = message.includes('rose'); |
|
|
|
if (message.includes('attack')) { |
|
triggerEffect(target, isIncrease ? 'attackUp' : 'attackDown', isIncrease ? '⚔️' : '🔻', 1000); |
|
} else if (message.includes('defense')) { |
|
triggerEffect(target, isIncrease ? 'defenseUp' : 'defenseDown', isIncrease ? '🛡️' : '🔻', 1000); |
|
} else if (message.includes('speed')) { |
|
triggerEffect(target, isIncrease ? 'speedUp' : 'speedDown', isIncrease ? '💨' : '🐌', 1000); |
|
} else if (message.includes('accuracy')) { |
|
triggerEffect(target, isIncrease ? 'accuracyUp' : 'accuracyDown', isIncrease ? '🎯' : '👁️', 1000); |
|
} |
|
} |
|
|
|
|
|
if (message.includes('recovered') && message.includes('HP')) { |
|
const target = message.includes(playerName) ? 'player' : 'enemy'; |
|
triggerEffect(target, 'heal', '💚', 1000); |
|
} |
|
|
|
|
|
if (message.includes('missed')) { |
|
triggerEffect('both', 'miss', '💫', 800); |
|
} |
|
} |
|
|
|
function triggerDamageFlash(target: 'player' | 'enemy') { |
|
if (target === 'player') { |
|
playerFlash = true; |
|
setTimeout(() => playerFlash = false, 600); |
|
} else { |
|
enemyFlash = true; |
|
setTimeout(() => enemyFlash = false, 600); |
|
} |
|
} |
|
|
|
function triggerEffect(target: 'player' | 'enemy' | 'both', type: string, emoji: string, duration: number) { |
|
const effect = { type, emoji, duration }; |
|
|
|
if (target === 'player' || target === 'both') { |
|
playerEffects = [...playerEffects, effect]; |
|
setTimeout(() => { |
|
playerEffects = playerEffects.filter(e => e !== effect); |
|
}, duration); |
|
} |
|
|
|
if (target === 'enemy' || target === 'both') { |
|
enemyEffects = [...enemyEffects, effect]; |
|
setTimeout(() => { |
|
enemyEffects = enemyEffects.filter(e => e !== effect); |
|
}, duration); |
|
} |
|
} |
|
|
|
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} |
|
{playerEffects} |
|
{enemyEffects} |
|
{playerFlash} |
|
{enemyFlash} |
|
/> |
|
|
|
<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> |