doll active state <-> doll visibility toggle

This commit is contained in:
2025-12-23 15:58:01 +08:00
parent b4234c12f5
commit c8efcfc83c
8 changed files with 228 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { writable } from "svelte/store";
import type { CursorPositions } from "../types/bindings/CursorPositions";
import type { CursorPosition } from "../types/bindings/CursorPosition";
import type { DollDto } from "../types/bindings/DollDto";
export let cursorPositionOnScreen = writable<CursorPositions>({
raw: { x: 0, y: 0 },
@@ -24,13 +25,13 @@ type FriendCursorData = {
// The exported store will only expose the position part to consumers,
// but internally we manage the full data.
// Actually, it's easier if we just export the positions and manage state internally.
export let friendsCursorPositions = writable<Record<string, CursorPositions>>(
{},
);
export let friendsCursorPositions = writable<Record<string, CursorPositions>>({});
export let friendsActiveDolls = writable<Record<string, DollDto | null>>({});
let unlistenCursor: UnlistenFn | null = null;
let unlistenFriendCursor: UnlistenFn | null = null;
let unlistenFriendDisconnected: UnlistenFn | null = null;
let unlistenFriendActiveDollChanged: UnlistenFn | null = null;
let isListening = false;
// Internal state to track timestamps
@@ -89,16 +90,19 @@ export async function initCursorTracking() {
try {
payload = JSON.parse(payload);
} catch (e) {
console.error("[Cursor] Failed to parse friend disconnected payload:", e);
console.error(
"[Cursor] Failed to parse friend disconnected payload:",
e,
);
return;
}
}
const data = Array.isArray(payload) ? payload[0] : payload;
// Remove from internal state
if (friendCursorState[data.userId]) {
delete friendCursorState[data.userId];
delete friendCursorState[data.userId];
}
// Update svelte store
@@ -107,9 +111,75 @@ export async function initCursorTracking() {
delete next[data.userId];
return next;
});
}
},
);
// Listen to friend active doll changed events
unlistenFriendActiveDollChanged = await listen<
| string
| {
friendId: string;
doll: DollDto | null;
}
>("friend-active-doll-changed", (event) => {
let data = event.payload;
if (typeof data === "string") {
try {
data = JSON.parse(data);
} catch (e) {
console.error(
"[Cursor] Failed to parse friend-active-doll-changed payload:",
e,
);
return;
}
}
// Cast to expected type after parsing
const payload = data as { friendId: string; doll: DollDto | null };
console.log(
"[Cursor] Received friend-active-doll-changed event:",
payload,
);
if (!payload.doll) {
// If doll is null, it means the friend deactivated their doll.
console.log(
`[Cursor] Removing doll for friend ${payload.friendId} due to deactivation`,
);
// Update the active dolls store to explicitly set this friend's doll to null
// We MUST set it to null instead of deleting it, otherwise the UI might
// fall back to the initial appData snapshot which might still have the old doll.
friendsActiveDolls.update((current) => {
const next = { ...current };
next[payload.friendId] = null;
return next;
});
// Also remove from cursor positions so the sprite disappears
friendsCursorPositions.update((current) => {
const next = { ...current };
delete next[payload.friendId];
return next;
});
} else {
// Update or add the new doll configuration
console.log(
`[Cursor] Updating doll for friend ${payload.friendId}:`,
payload.doll,
);
friendsActiveDolls.update((current) => {
return {
...current,
[payload.friendId]: payload.doll!,
};
});
}
});
isListening = true;
} catch (err) {
console.error("[Cursor] Failed to initialize cursor tracking:", err);
@@ -134,6 +204,10 @@ export function stopCursorTracking() {
unlistenFriendDisconnected();
unlistenFriendDisconnected = null;
}
if (unlistenFriendActiveDollChanged) {
unlistenFriendActiveDollChanged();
unlistenFriendActiveDollChanged = null;
}
isListening = false;
}

View File

@@ -2,6 +2,7 @@
import {
cursorPositionOnScreen,
friendsCursorPositions,
friendsActiveDolls,
} from "../../events/cursor";
import { appData } from "../../events/app-data";
@@ -14,6 +15,18 @@
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
return friend ? friend.friend.name : userId.slice(0, 8) + "...";
}
function getFriendDollConfig(userId: string) {
// 1. Try to get from real-time store (most up-to-date)
// Check if key exists to distinguish between "unknown" (undefined) and "no doll" (null)
if (userId in $friendsActiveDolls) {
return $friendsActiveDolls[userId]?.configuration;
}
// 2. Fallback to initial app data (snapshot on load)
const friend = $appData?.friends?.find((f) => f.friend.id === userId);
return friend?.friend.activeDoll?.configuration;
}
</script>
<svelte:window bind:innerWidth bind:innerHeight />
@@ -66,11 +79,16 @@
<div class="absolute inset-0 size-full">
{#if Object.keys($friendsCursorPositions).length > 0}
{#each Object.entries($friendsCursorPositions) as [userId, position]}
<DesktopPet
targetX={position.mapped.x * innerWidth}
targetY={position.mapped.y * innerHeight}
name={getFriendName(userId)}
/>
{@const config = getFriendDollConfig(userId)}
{#if config}
<DesktopPet
targetX={position.mapped.x * innerWidth}
targetY={position.mapped.y * innerHeight}
name={getFriendName(userId)}
bodyColor={config?.colorScheme?.body}
outlineColor={config?.colorScheme?.outline}
/>
{/if}
{/each}
{/if}
</div>

View File

@@ -1,10 +1,13 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import onekoGif from "../../assets/oneko/oneko.gif";
export let targetX = 0;
export let targetY = 0;
export let name = "";
export let bodyColor: string | undefined = undefined;
export let outlineColor: string | undefined = undefined;
let nekoPosX = 32;
let nekoPosY = 32;
@@ -18,6 +21,35 @@
let animationFrameId: number;
let lastFrameTimestamp: number;
let spriteSheetUrl = onekoGif; // Default to standard GIF
let isCustomSprite = false;
// Watch for color changes to regenerate sprite
$: if (bodyColor && outlineColor) {
updateSpriteSheet(bodyColor, outlineColor);
} else {
// Revert to default if colors are missing
spriteSheetUrl = onekoGif;
isCustomSprite = false;
}
async function updateSpriteSheet(body: string, outline: string) {
try {
const result = await invoke<string>("recolor_gif_base64", {
whiteColorHex: body,
blackColorHex: outline,
applyTexture: true,
});
spriteSheetUrl = `data:image/gif;base64,${result}`;
isCustomSprite = true;
} catch (e) {
console.error("Failed to recolor sprite:", e);
// Fallback
spriteSheetUrl = onekoGif;
isCustomSprite = false;
}
}
// Sprite constants from oneko.js
const spriteSets: Record<string, [number, number][]> = {
idle: [[-3, -3]],
@@ -231,7 +263,7 @@
style="
width: 32px;
height: 32px;
background-image: url({onekoGif});
background-image: url('{spriteSheetUrl}');
background-position: {currentSprite.x}px {currentSprite.y}px;
"
></div>

View File

@@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollDto } from "./DollDto";
export type UserBasicDto = { id: string, name: string, username: string | null, activeDollId: string | null, };
export type UserBasicDto = { id: string, name: string, username: string | null, activeDoll: DollDto | null, };