piclets / src /lib /components /Battle /BattleField.svelte
Fraser's picture
rm legacy
1f2c086
<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;
// Animation states
let playerVisible = false;
let enemyVisible = false;
// Trainer animation states
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;
}
// For trainer battles, enemy will appear when enemyTrainerSlideOut triggers
// For all battles, player will appear when playerTrainerSlideOut triggers
}
});
// Watch for trainer slide-out triggers
$: 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); // Time for slide-out animation
}
$: 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); // Time for slide-out animation
}
</script>
<div class="battle-field">
<!-- White flash overlay -->
{#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">
<!-- Field Effects Display -->
{#if battleState?.fieldEffects}
<FieldEffectIndicator fieldEffects={battleState.fieldEffects} />
{/if}
<!-- Enemy Row -->
<div class="enemy-row">
<div class="enemy-stack" class:intro-animations={showIntro}>
<PicletInfo
piclet={enemyPiclet}
hpPercentage={enemyHpPercentage}
isPlayer={false}
/>
<!-- Static Enemy Platform (always visible) -->
<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}>
<!-- Enemy Battle Effects wrap the image for flicker animation -->
<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>
<!-- Enemy Status Effects -->
{#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}>
<!-- Static Player Platform (always visible) -->
<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}>
<!-- Player Battle Effects wrap the image for flicker animation -->
<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 */
.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 */
.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 fallbacks */
.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 images */
.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;
}
/* Status Effects Positioning */
.enemy-status-effects {
position: absolute;
top: -10px;
right: -10px;
z-index: 5;
}
.player-status-effects {
position: absolute;
bottom: -10px;
left: -10px;
z-index: 5;
}
/* Animations */
.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);
}
}
/* Info box animations */
.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>