fix(web): mascot physics, drag reliability, and speech-bubble polish
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

Audits and fixes ground-teleport/flat-fall/toss-momentum physics bugs,
fixes drag getting stuck via missing pointercancel handling, replaces
sprite-based speech bubbles with real HTML text/emoji bubbles, adds
drag-onto-icon "investigate" reactions and idle chatter, merges the
name badge and reaction bubble into one floating element, and caps the
bubble to one line with a teleprompter-style auto-scroll instead of
ellipsizing overflow text.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 23:23:30 +02:00
parent 7b1dfbc8aa
commit d82095213a
16 changed files with 929 additions and 140 deletions

View File

@@ -48,9 +48,11 @@
reactAnim: null,
bounds: { w: 800, h: 600 },
groundY: 600,
bubble: null,
bubbleText: null,
bubbleUntil: 0,
blinkUntil: 0
blinkUntil: 0,
fallPhaseAt: 0,
investigateOnLand: false
})
let menuPos = $state<{ x: number; y: number } | null>(null)
@@ -105,7 +107,7 @@
function onPet() {
// Plain-click pet: briefly show a happy bubble. The actual model.pet()
// call already happened in Mascot.svelte.
runtime.bubble = '/mascot/bubble-love.png'
runtime.bubbleText = '❤️'
runtime.bubbleUntil = performance.now() + 1500
}
@@ -149,6 +151,13 @@
// behind it.
const detach = attachStimuli((reaction) => {
if (getModel().stage === 'egg') return
// Dragging always wins — never let a reaction interrupt an active
// drag (previously it could visually flash a reaction animation
// mid-drag, even though position tracking stayed correct; see the
// physics audit's Finding 5). Sleep is only interrupted by
// reactions that explicitly opt in via interruptsSleep.
if (runtime.behavior === 'dragged') return
if (runtime.behavior === 'sleep' && !reaction.interruptsSleep) return
// Reaction dispatch: respect priority + cooldown (handled in stimuli.ts);
// here we just force the behavior.
const anim = reaction.anim as AnimName
@@ -156,7 +165,7 @@
runtime.reactAnim = anim
forceBehavior(runtime, id, { anim, durationMs: reaction.durationMs })
if (reaction.bubble) {
runtime.bubble = reaction.bubble
runtime.bubbleText = reaction.bubble
runtime.bubbleUntil = performance.now() + reaction.durationMs
}
if (reaction.effect) reaction.effect()
@@ -180,9 +189,13 @@
<div bind:this={host} class="pointer-events-none absolute inset-0 z-[45]">
<Mascot
{runtime}
bind:runtime
onContextMenu={openMenu}
onPet={onPet}
onRequestName={() => {
nameDialogMode = 'hatch'
nameDialogOpen = true
}}
/>
</div>