interaction system
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
|
||||
import PetSprite from "$lib/components/PetSprite.svelte";
|
||||
import onekoGif from "../../../assets/oneko/oneko.gif";
|
||||
import {
|
||||
receivedInteractions,
|
||||
clearInteraction,
|
||||
} from "$lib/stores/interaction-store";
|
||||
import PetMenu from "./PetMenu.svelte";
|
||||
import type { DollDto } from "../../../types/bindings/DollDto";
|
||||
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
||||
@@ -26,6 +30,40 @@
|
||||
let spriteSheetUrl = onekoGif;
|
||||
|
||||
let isPetMenuOpen = false;
|
||||
let receivedMessage: string | undefined = undefined;
|
||||
let messageTimer: number | undefined = undefined;
|
||||
|
||||
// Watch for received interactions for this user
|
||||
$: {
|
||||
const interaction = $receivedInteractions.get(user.id);
|
||||
if (interaction && interaction.content !== receivedMessage) {
|
||||
console.log(`Received interaction for ${user.id}: ${interaction.content}`);
|
||||
receivedMessage = interaction.content;
|
||||
isPetMenuOpen = true;
|
||||
|
||||
// Make scene interactive so user can see it
|
||||
invoke("set_scene_interactive", {
|
||||
interactive: true,
|
||||
shouldClick: false,
|
||||
});
|
||||
|
||||
// Clear existing timer if any
|
||||
if (messageTimer) clearTimeout(messageTimer);
|
||||
|
||||
// Auto-close and clear after 8 seconds
|
||||
messageTimer = setTimeout(() => {
|
||||
isPetMenuOpen = false;
|
||||
receivedMessage = undefined;
|
||||
clearInteraction(user.id);
|
||||
// We probably shouldn't disable interactivity globally here as other pets might be active,
|
||||
// but 'set_pet_menu_state' in backend handles the window transparency logic per pet/menu.
|
||||
// However, we did explicitly call set_scene_interactive(true).
|
||||
// It might be safer to let the mouse-leave or other logic handle setting it back to false,
|
||||
// or just leave it as is since the user might want to interact.
|
||||
// For now, focusing on the message lifecycle.
|
||||
}, 8000) as unknown as number;
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for color changes to regenerate sprite
|
||||
$: updateSprite(
|
||||
@@ -33,10 +71,22 @@
|
||||
doll?.configuration.colorScheme.outline,
|
||||
);
|
||||
|
||||
$: (isInteractive, (isPetMenuOpen = false));
|
||||
// This reactive statement forces the menu closed whenever `isInteractive` changes.
|
||||
// This conflicts with our message logic because we explicitly set interactive=true when opening the menu for a message.
|
||||
// We should remove this or condition it.
|
||||
// The original intent was likely to close the menu if the user moves the mouse away (interactive becomes false),
|
||||
// but `isInteractive` is driven by mouse hover usually.
|
||||
// When we force it via invoke("set_scene_interactive", { interactive: true }), it might not reflect back into `isInteractive` prop immediately or correctly depending on how the parent passes it.
|
||||
// Actually, `isInteractive` is a prop passed from +page.svelte probably based on hover state.
|
||||
// If we want the menu to stay open during the message, we should probably ignore this auto-close behavior if a message is present.
|
||||
|
||||
$: if (!receivedMessage && !isInteractive) {
|
||||
isPetMenuOpen = false;
|
||||
}
|
||||
|
||||
$: {
|
||||
if (id) {
|
||||
console.log(`Setting pet menu state for ${id}: ${isPetMenuOpen}`);
|
||||
invoke("set_pet_menu_state", { id, open: isPetMenuOpen });
|
||||
}
|
||||
}
|
||||
@@ -99,13 +149,19 @@
|
||||
aria-label="Pet Menu"
|
||||
>
|
||||
{#if doll}
|
||||
<PetMenu {doll} {user} />
|
||||
<PetMenu {doll} {user} {receivedMessage} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => {
|
||||
isPetMenuOpen = !isPetMenuOpen;
|
||||
if (!isPetMenuOpen) {
|
||||
// Clear message when closing menu manually
|
||||
receivedMessage = undefined;
|
||||
clearInteraction(user.id);
|
||||
if (messageTimer) clearTimeout(messageTimer);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PetSprite
|
||||
|
||||
@@ -1,23 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { type DollDto } from "../../../types/bindings/DollDto";
|
||||
import type { UserBasicDto } from "../../../types/bindings/UserBasicDto";
|
||||
import type { SendInteractionDto } from "../../../types/bindings/SendInteractionDto";
|
||||
|
||||
export let doll: DollDto;
|
||||
export let user: UserBasicDto;
|
||||
export let receivedMessage: string | undefined = undefined;
|
||||
|
||||
let showMessageInput = false;
|
||||
let messageContent = "";
|
||||
|
||||
async function sendMessage() {
|
||||
if (!messageContent.trim()) return;
|
||||
|
||||
const dto: SendInteractionDto = {
|
||||
recipientUserId: user.id,
|
||||
content: messageContent,
|
||||
type: "text",
|
||||
};
|
||||
|
||||
try {
|
||||
await invoke("send_interaction_cmd", { dto });
|
||||
messageContent = "";
|
||||
showMessageInput = false;
|
||||
} catch (e) {
|
||||
console.error("Failed to send interaction:", e);
|
||||
alert("Failed to send message: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Enter") {
|
||||
sendMessage();
|
||||
} else if (event.key === "Escape") {
|
||||
showMessageInput = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bg-base-100 border border-base-200 card p-1">
|
||||
<div class="bg-base-100 border border-base-200 card p-1 min-w-[150px]">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-row w-full items-end gap-1">
|
||||
<p class="text-sm font-semibold">{doll.name}</p>
|
||||
<p class="text-[0.6rem] opacity-50">From {user.name}</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||
<button>Headpat</button>
|
||||
<button>Message</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||
<button>Postcard</button>
|
||||
<button>Wormhole</button>
|
||||
</div>
|
||||
|
||||
{#if receivedMessage}
|
||||
<div class="">
|
||||
<div class="text-sm max-w-[140px]">
|
||||
{receivedMessage}
|
||||
</div>
|
||||
</div>
|
||||
{:else if showMessageInput}
|
||||
<div class="flex flex-col gap-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={messageContent}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Type message..."
|
||||
class="input input-xs input-bordered w-full"
|
||||
autofocus
|
||||
/>
|
||||
<div class="flex gap-1">
|
||||
<button class="btn btn-xs btn-primary flex-1" onclick={sendMessage}
|
||||
>Send</button
|
||||
>
|
||||
<button
|
||||
class="btn btn-xs flex-1"
|
||||
onclick={() => (showMessageInput = false)}>Cancel</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||
<button disabled>Headpat</button>
|
||||
<button onclick={() => (showMessageInput = true)}>Message</button>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1 w-full *:flex-1 *:btn *:btn-sm">
|
||||
<button disabled>Postcard</button>
|
||||
<button disabled>Wormhole</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user