added sender sprite next to petpet

This commit is contained in:
2026-03-05 18:44:39 +08:00
parent a272b5f286
commit 0e6b497cf6
2 changed files with 67 additions and 4 deletions

View File

@@ -16,6 +16,7 @@
import { receivedInteractions, clearInteraction } from "$lib/stores/interaction-store";
import { INTERACTION_TYPE_HEADPAT } from "$lib/constants/interaction";
import { listen } from "@tauri-apps/api/event";
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
import { onMount } from "svelte";
import type { PresenceStatus } from "../../types/bindings/PresenceStatus";
import type { DollDto } from "../../types/bindings/DollDto";
@@ -28,27 +29,57 @@
// Fullscreen modal state for headpats
let showFullscreenModal = $state(false);
let fullscreenImageSrc = $state("");
let headpatSenderSpriteUrl = $state("");
let headpatSenderId = $state<string | null>(null);
let headpatTimer: ReturnType<typeof setTimeout> | null = null;
// Queue for pending headpats (when modal is already showing)
let headpatQueue = $state<Array<{ userId: string; content: string }>>([]);
let headpatSpriteToken = 0;
// Process next headpat in queue
function processNextHeadpat() {
if (headpatQueue.length > 0) {
const next = headpatQueue.shift()!;
clearInteraction(next.userId);
fullscreenImageSrc = `data:image/gif;base64,${next.content}`;
headpatSenderId = next.userId;
void loadHeadpatSprites(next.userId);
showFullscreenModal = true;
scheduleHeadpatDismiss();
} else {
fullscreenImageSrc = "";
headpatSenderSpriteUrl = "";
headpatSenderId = null;
}
}
async function loadHeadpatSprites(senderId: string) {
const token = ++headpatSpriteToken;
const senderDoll = getFriendDoll(senderId);
const userDoll = getUserDoll();
let userPetpetGif = "";
if (userDoll) {
try {
const gifBase64 = await invoke<string>("encode_pet_doll_gif_base64", { doll: userDoll });
userPetpetGif = `data:image/gif;base64,${gifBase64}`;
} catch (e) {
console.error("Failed to generate user petpet:", e);
}
}
const senderSpriteUrl = senderDoll
? await getSpriteSheetUrl({
bodyColor: senderDoll.configuration.colorScheme.body,
outlineColor: senderDoll.configuration.colorScheme.outline,
})
: await getSpriteSheetUrl();
if (token !== headpatSpriteToken) return;
fullscreenImageSrc = userPetpetGif;
headpatSenderSpriteUrl = senderSpriteUrl;
}
function scheduleHeadpatDismiss() {
if (headpatTimer) {
clearTimeout(headpatTimer);
@@ -81,8 +112,8 @@
} else {
// Show immediately and clear from store
clearInteraction(userId);
fullscreenImageSrc = `data:image/gif;base64,${interaction.content}`;
headpatSenderId = userId;
void loadHeadpatSprites(userId);
showFullscreenModal = true;
scheduleHeadpatDismiss();
}
@@ -266,6 +297,7 @@
<FullscreenModal
bind:visible={showFullscreenModal}
imageSrc={fullscreenImageSrc}
senderSpriteUrl={headpatSenderSpriteUrl}
senderName={getHeadpatSenderName(headpatSenderId)}
/>
</div>

View File

@@ -1,6 +1,8 @@
<script lang="ts">
import { cubicOut } from "svelte/easing";
import { type TransitionConfig } from "svelte/transition";
import PetSprite from "$lib/components/PetSprite.svelte";
import { SPRITE_SETS, SPRITE_SIZE } from "$lib/constants/pet-sprites";
function fadeSlide(
node: HTMLElement,
@@ -14,11 +16,22 @@
};
}
const idleSprite = {
x: SPRITE_SETS.idle[0][0] * SPRITE_SIZE,
y: SPRITE_SETS.idle[0][1] * SPRITE_SIZE,
};
let {
imageSrc,
senderSpriteUrl = "",
visible = $bindable(false),
senderName = "",
}: { imageSrc: string; visible: boolean; senderName?: string } = $props();
}: {
imageSrc: string;
senderSpriteUrl?: string;
visible: boolean;
senderName?: string;
} = $props();
</script>
@@ -30,6 +43,24 @@
{#if senderName}
<div class="mb-4 text-white text-lg font-medium">{senderName} gave you a headpat!</div>
{/if}
<img src={imageSrc} alt="Headpat" class="max-w-full max-h-full object-contain" />
<div class="flex items-center justify-center gap-6">
{#if senderSpriteUrl}
<div class="flex items-center justify-center size-32">
<div style="transform: scale(4); transform-origin: center;">
<PetSprite
spriteSheetUrl={senderSpriteUrl}
spriteX={idleSprite.x}
spriteY={idleSprite.y}
size={32}
/>
</div>
</div>
{/if}
<img
src={imageSrc}
alt="Headpat"
class="max-w-full max-h-full object-contain"
/>
</div>
</div>
{/if}