|
<script lang="ts"> |
|
import { onMount } from 'svelte'; |
|
import { fade } from 'svelte/transition'; |
|
import type { PicletInstance } from '$lib/db/schema'; |
|
import type { BattleState } from '$lib/battle-engine/types'; |
|
import PicletInfo from './PicletInfo.svelte'; |
|
import StatusEffectIndicator from './StatusEffectIndicator.svelte'; |
|
import FieldEffectIndicator from './FieldEffectIndicator.svelte'; |
|
import BattleEffects from './BattleEffects.svelte'; |
|
|
|
export let playerPiclet: PicletInstance; |
|
export let enemyPiclet: PicletInstance; |
|
export let playerHpPercentage: number; |
|
export let enemyHpPercentage: number; |
|
export let showIntro: boolean = false; |
|
export let battleState: BattleState | undefined = undefined; |
|
export let playerEffects: Array<{type: string, emoji: string, duration: number}> = []; |
|
export let enemyEffects: Array<{type: string, emoji: string, duration: number}> = []; |
|
export let playerFlash: boolean = false; |
|
export let enemyFlash: boolean = false; |
|
export let playerFaint: boolean = false; |
|
export let enemyFaint: boolean = false; |
|
export let playerLunge: boolean = false; |
|
export let enemyLunge: boolean = false; |
|
export let isWildBattle: boolean = true; |
|
export let showWhiteFlash: boolean = false; |
|
export let playerTrainerVisible: boolean = false; |
|
export let enemyTrainerVisible: boolean = false; |
|
export let playerTrainerSlideOut: boolean = false; |
|
export let enemyTrainerSlideOut: boolean = false; |
|
|
|
|
|
let playerVisible = false; |
|
let enemyVisible = false; |
|
|
|
|
|
let playerTrainerSliding = false; |
|
let enemyTrainerSliding = false; |
|
|
|
|
|
onMount(() => { |
|
if (!showIntro) { |
|
// Skip intro - show everything immediately |
|
playerVisible = true; |
|
enemyVisible = true; |
|
} else { |
|
// In wild battles, enemy Piclet should be visible from start |
|
if (isWildBattle) { |
|
enemyVisible = true; |
|
} |
|
|
|
|
|
} |
|
}); |
|
|
|
|
|
$: if (playerTrainerSlideOut && !playerTrainerSliding) { |
|
playerTrainerSliding = true; |
|
// Show trainer and let CSS handle slide-out animation |
|
playerTrainerVisible = true; |
|
|
|
// After slide animation completes, hide trainer and show Piclet |
|
setTimeout(() => { |
|
playerTrainerVisible = false; |
|
// Trigger white flash then show player monster |
|
showWhiteFlash = true; |
|
setTimeout(() => { |
|
showWhiteFlash = false; |
|
playerVisible = true; |
|
}, 300); |
|
}, 600); |
|
} |
|
|
|
$: if (enemyTrainerSlideOut && !enemyTrainerSliding) { |
|
enemyTrainerSliding = true; |
|
// Show trainer and let CSS handle slide-out animation |
|
enemyTrainerVisible = true; |
|
|
|
// After slide animation completes, hide trainer and show Piclet |
|
setTimeout(() => { |
|
enemyTrainerVisible = false; |
|
// Trigger white flash then show enemy monster |
|
showWhiteFlash = true; |
|
setTimeout(() => { |
|
showWhiteFlash = false; |
|
enemyVisible = true; |
|
}, 300); |
|
}, 600); |
|
} |
|
</script> |
|
|
|
<div class="battle-field"> |
|
|
|
{#if showWhiteFlash} |
|
<div class="white-flash" transition:fade={{ duration: 300 }}></div> |
|
{/if} |
|
|
|
|
|
<!-- Player Trainer --> |
|
{#if playerTrainerVisible} |
|
<div class="player-trainer" class:slide-out-left={playerTrainerSlideOut}> |
|
<img src="/assets/default_trainer.png" alt="Player Trainer" /> |
|
</div> |
|
{/if} |
|
|
|
<!-- Enemy Trainer (only for trainer battles) --> |
|
{#if !isWildBattle && enemyTrainerVisible} |
|
<div class="enemy-trainer" class:slide-out-right={enemyTrainerSlideOut}> |
|
<img src="/assets/default_trainer.png" alt="Enemy Trainer" /> |
|
</div> |
|
{/if} |
|
|
|
<div class="battle-content"> |
|
|
|
{#if battleState?.fieldEffects} |
|
<FieldEffectIndicator fieldEffects={battleState.fieldEffects} /> |
|
{/if} |
|
|
|
|
|
<div class="enemy-row"> |
|
<div class="enemy-stack" class:intro-animations={showIntro}> |
|
<PicletInfo |
|
piclet={enemyPiclet} |
|
hpPercentage={enemyHpPercentage} |
|
isPlayer={false} |
|
/> |
|
|
|
|
|
<img |
|
class="platform enemy-platform" |
|
src="/assets/grass.PNG" |
|
alt="Platform" |
|
on:error={(e) => { |
|
const target = e.currentTarget as HTMLImageElement; |
|
const nextSibling = target.nextElementSibling as HTMLElement; |
|
target.style.display = 'none'; |
|
if (nextSibling) nextSibling.style.display = 'block'; |
|
}} |
|
/> |
|
<div class="platform-fallback enemy-platform-fallback" style="display: none;"></div> |
|
|
|
{#if enemyVisible} |
|
<div class="enemy-piclet-wrapper" class:animate-in={showIntro} class:lunge={enemyLunge}> |
|
|
|
<BattleEffects effects={enemyEffects} flash={enemyFlash} faint={enemyFaint}> |
|
<img |
|
class="piclet-image enemy-image" |
|
src={enemyPiclet.imageData || enemyPiclet.imageUrl} |
|
alt={enemyPiclet.nickname} |
|
on:error={(e) => { |
|
const target = e.currentTarget as HTMLImageElement; |
|
target.src = 'https://via.placeholder.com/120x120?text=Piclet'; |
|
}} |
|
/> |
|
</BattleEffects> |
|
|
|
|
|
{#if battleState?.opponentPiclet?.statusEffects} |
|
<div class="enemy-status-effects"> |
|
<StatusEffectIndicator statusEffects={battleState.opponentPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} /> |
|
</div> |
|
{/if} |
|
</div> |
|
{/if} |
|
</div> |
|
</div> |
|
|
|
<div class="spacer"></div> |
|
|
|
<!-- Player Row --> |
|
<div class="player-row"> |
|
<div class="player-stack" class:intro-animations={showIntro}> |
|
|
|
|
|
<img |
|
class="platform player-platform" |
|
src="/assets/grass.PNG" |
|
alt="Platform" |
|
on:error={(e) => { |
|
const target = e.currentTarget as HTMLImageElement; |
|
const nextSibling = target.nextElementSibling as HTMLElement; |
|
target.style.display = 'none'; |
|
if (nextSibling) nextSibling.style.display = 'block'; |
|
}} |
|
/> |
|
<div class="platform-fallback player-platform-fallback" style="display: none;"></div> |
|
|
|
{#if playerVisible} |
|
<div class="player-piclet-wrapper" class:animate-in={showIntro} class:lunge={playerLunge}> |
|
|
|
<BattleEffects effects={playerEffects} flash={playerFlash} faint={playerFaint}> |
|
<img |
|
class="piclet-image player-image" |
|
src={playerPiclet.imageData || playerPiclet.imageUrl} |
|
alt={playerPiclet.nickname} |
|
on:error={(e) => { |
|
const target = e.currentTarget as HTMLImageElement; |
|
target.src = 'https://via.placeholder.com/120x120?text=Piclet'; |
|
}} |
|
/> |
|
</BattleEffects> |
|
|
|
<!-- Player Status Effects --> |
|
{#if battleState?.playerPiclet?.statusEffects} |
|
<div class="player-status-effects"> |
|
<StatusEffectIndicator statusEffects={battleState.playerPiclet.statusEffects.map(effect => ({ type: effect, turnsLeft: 3 }))} /> |
|
</div> |
|
{/if} |
|
</div> |
|
{/if} |
|
|
|
<PicletInfo |
|
piclet={playerPiclet} |
|
hpPercentage={playerHpPercentage} |
|
isPlayer={true} |
|
/> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<style> |
|
.battle-field { |
|
height: 280px; |
|
position: relative; |
|
overflow: hidden; |
|
background: repeating-linear-gradient( |
|
to bottom, |
|
rgba(76, 175, 80, 0.2) 0px, |
|
rgba(76, 175, 80, 0.2) 5px, |
|
rgba(76, 175, 80, 0.1) 5px, |
|
rgba(76, 175, 80, 0.1) 10px |
|
); |
|
} |
|
|
|
.white-flash { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
right: 0; |
|
bottom: 0; |
|
background: white; |
|
z-index: 50; |
|
} |
|
|
|
|
|
.player-trainer { |
|
position: absolute; |
|
bottom: 20px; |
|
left: 20px; |
|
z-index: 10; |
|
transition: transform 0.6s ease-in-out; |
|
} |
|
|
|
.player-trainer img { |
|
width: 120px; |
|
height: auto; |
|
/* Remove horizontal flip - trainer should face naturally */ |
|
} |
|
|
|
.player-trainer.slide-out-left { |
|
transform: translateX(-200px); |
|
} |
|
|
|
.enemy-trainer { |
|
position: absolute; |
|
top: 20px; |
|
right: 20px; |
|
z-index: 10; |
|
transition: transform 0.6s ease-in-out; |
|
} |
|
|
|
.enemy-trainer img { |
|
width: 120px; |
|
height: auto; |
|
} |
|
|
|
.enemy-trainer.slide-out-right { |
|
transform: translateX(200px); |
|
} |
|
|
|
|
|
.battle-content { |
|
display: flex; |
|
flex-direction: column; |
|
height: 100%; |
|
} |
|
|
|
|
|
.enemy-row { |
|
flex: 1; |
|
position: relative; |
|
} |
|
|
|
.enemy-stack { |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
left: 0; |
|
bottom: 0; |
|
} |
|
|
|
.enemy-piclet-wrapper { |
|
position: absolute; |
|
right: 40px; |
|
top: 0; |
|
} |
|
|
|
.enemy-image { |
|
width: 120px; |
|
height: 120px; |
|
object-fit: contain; |
|
display: block; |
|
} |
|
|
|
.enemy-platform { |
|
width: 160px; |
|
height: 160px; |
|
position: absolute; |
|
bottom: -60px; |
|
right: 20px; /* Align with enemy-piclet-wrapper position */ |
|
z-index: 0; |
|
object-fit: cover; |
|
} |
|
|
|
|
|
.player-row { |
|
height: 140px; |
|
position: relative; |
|
} |
|
|
|
.player-stack { |
|
position: relative; |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
.player-piclet-wrapper { |
|
position: absolute; |
|
left: 40px; |
|
bottom: 0; |
|
} |
|
|
|
.player-image { |
|
width: 120px; |
|
height: 120px; |
|
object-fit: contain; |
|
display: block; |
|
transform: scaleX(-1); |
|
} |
|
|
|
.player-platform { |
|
width: 160px; |
|
height: 160px; |
|
position: absolute; |
|
bottom: -80px; |
|
left: 20px; /* Align with player-piclet-wrapper position */ |
|
z-index: 0; |
|
object-fit: cover; |
|
} |
|
|
|
|
|
.platform-fallback { |
|
position: absolute; |
|
background: rgba(76, 175, 80, 0.3); |
|
border-radius: 50%; |
|
} |
|
|
|
.enemy-platform-fallback { |
|
width: 160px; |
|
height: 160px; |
|
bottom: -60px; |
|
right: 20px; |
|
} |
|
|
|
.player-platform-fallback { |
|
width: 160px; |
|
height: 160px; |
|
bottom: -80px; |
|
left: 20px; |
|
} |
|
|
|
|
|
.piclet-image { |
|
image-rendering: auto; |
|
filter: drop-shadow(-2px 0 4px rgba(0, 0, 0, 0.1)); |
|
position: relative; |
|
z-index: 1; |
|
} |
|
|
|
.spacer { |
|
flex: 1; |
|
} |
|
|
|
|
|
.enemy-status-effects { |
|
position: absolute; |
|
top: -10px; |
|
right: -10px; |
|
z-index: 5; |
|
} |
|
|
|
.player-status-effects { |
|
position: absolute; |
|
bottom: -10px; |
|
left: -10px; |
|
z-index: 5; |
|
} |
|
|
|
|
|
.enemy-piclet-wrapper { |
|
animation-fill-mode: both; |
|
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
|
} |
|
|
|
.enemy-piclet-wrapper.animate-in { |
|
animation: enemySlideIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
|
} |
|
|
|
.enemy-piclet-wrapper.lunge { |
|
animation: enemyLunge 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
|
} |
|
|
|
.player-piclet-wrapper { |
|
animation-fill-mode: both; |
|
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
|
} |
|
|
|
.player-piclet-wrapper.animate-in { |
|
animation: playerSlideIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
|
} |
|
|
|
.player-piclet-wrapper.lunge { |
|
animation: playerLunge 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94); |
|
} |
|
|
|
@keyframes enemySlideIn { |
|
0% { |
|
transform: translateX(150px) translateY(-50px) scale(1.5); |
|
opacity: 0; |
|
} |
|
50% { |
|
opacity: 1; |
|
} |
|
100% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
opacity: 1; |
|
} |
|
} |
|
|
|
@keyframes playerSlideIn { |
|
0% { |
|
transform: translateX(-150px) translateY(50px) scale(0.5); |
|
opacity: 0; |
|
} |
|
50% { |
|
opacity: 1; |
|
} |
|
100% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
opacity: 1; |
|
} |
|
} |
|
|
|
@keyframes enemyLunge { |
|
0% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
} |
|
40% { |
|
transform: translateX(-60px) translateY(10px) scale(1.1); |
|
} |
|
100% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
} |
|
} |
|
|
|
@keyframes playerLunge { |
|
0% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
} |
|
40% { |
|
transform: translateX(60px) translateY(-10px) scale(1.1); |
|
} |
|
100% { |
|
transform: translateX(0) translateY(0) scale(1); |
|
} |
|
} |
|
|
|
|
|
.enemy-stack.intro-animations :global(.piclet-info-wrapper) { |
|
animation: fadeSlideDown 0.5s ease-out 0.3s both; |
|
} |
|
|
|
.player-stack.intro-animations :global(.piclet-info-wrapper) { |
|
animation: fadeSlideUp 0.5s ease-out 0.3s both; |
|
} |
|
|
|
@keyframes fadeSlideDown { |
|
0% { |
|
opacity: 0; |
|
transform: translateY(-20px); |
|
} |
|
100% { |
|
opacity: 1; |
|
transform: translateY(0); |
|
} |
|
} |
|
|
|
@keyframes fadeSlideUp { |
|
0% { |
|
opacity: 0; |
|
transform: translateY(20px); |
|
} |
|
100% { |
|
opacity: 1; |
|
transform: translateY(0); |
|
} |
|
} |
|
|
|
@media (max-width: 768px) { |
|
.enemy-image { |
|
width: 120px; |
|
height: 120px; |
|
} |
|
|
|
.player-image { |
|
width: 120px; |
|
height: 120px; |
|
} |
|
|
|
.enemy-piclet-wrapper { |
|
right: 40px; |
|
} |
|
|
|
.player-piclet-wrapper { |
|
left: 40px; |
|
} |
|
} |
|
</style> |