diff --git a/src/renderer/src/assets/emissary/angry.png b/src/renderer/src/assets/emissary/angry.png new file mode 100644 index 0000000..d2b291d Binary files /dev/null and b/src/renderer/src/assets/emissary/angry.png differ diff --git a/src/renderer/src/assets/emissary/ashamed.png b/src/renderer/src/assets/emissary/ashamed.png new file mode 100644 index 0000000..36b3ba5 Binary files /dev/null and b/src/renderer/src/assets/emissary/ashamed.png differ diff --git a/src/renderer/src/assets/emissary/awaiting.png b/src/renderer/src/assets/emissary/awaiting.png new file mode 100644 index 0000000..72b06f5 Binary files /dev/null and b/src/renderer/src/assets/emissary/awaiting.png differ diff --git a/src/renderer/src/assets/emissary/blowing-bubbles.png b/src/renderer/src/assets/emissary/blowing-bubbles.png new file mode 100644 index 0000000..8dbae6a Binary files /dev/null and b/src/renderer/src/assets/emissary/blowing-bubbles.png differ diff --git a/src/renderer/src/assets/emissary/bored.png b/src/renderer/src/assets/emissary/bored.png new file mode 100644 index 0000000..8e8c195 Binary files /dev/null and b/src/renderer/src/assets/emissary/bored.png differ diff --git a/src/renderer/src/assets/emissary/broody.png b/src/renderer/src/assets/emissary/broody.png new file mode 100644 index 0000000..0570fe9 Binary files /dev/null and b/src/renderer/src/assets/emissary/broody.png differ diff --git a/src/renderer/src/assets/emissary/browsing.png b/src/renderer/src/assets/emissary/browsing.png new file mode 100644 index 0000000..8cf01bc Binary files /dev/null and b/src/renderer/src/assets/emissary/browsing.png differ diff --git a/src/renderer/src/assets/emissary/cheeky-2.png b/src/renderer/src/assets/emissary/cheeky-2.png new file mode 100644 index 0000000..1bacf84 Binary files /dev/null and b/src/renderer/src/assets/emissary/cheeky-2.png differ diff --git a/src/renderer/src/assets/emissary/cheeky-grin.png b/src/renderer/src/assets/emissary/cheeky-grin.png new file mode 100644 index 0000000..f462ac9 Binary files /dev/null and b/src/renderer/src/assets/emissary/cheeky-grin.png differ diff --git a/src/renderer/src/assets/emissary/cheeky.png b/src/renderer/src/assets/emissary/cheeky.png new file mode 100644 index 0000000..c2ec0f9 Binary files /dev/null and b/src/renderer/src/assets/emissary/cheeky.png differ diff --git a/src/renderer/src/assets/emissary/chortle.png b/src/renderer/src/assets/emissary/chortle.png new file mode 100644 index 0000000..72b8452 Binary files /dev/null and b/src/renderer/src/assets/emissary/chortle.png differ diff --git a/src/renderer/src/assets/emissary/chuckling.png b/src/renderer/src/assets/emissary/chuckling.png new file mode 100644 index 0000000..2350d95 Binary files /dev/null and b/src/renderer/src/assets/emissary/chuckling.png differ diff --git a/src/renderer/src/assets/emissary/concussion.png b/src/renderer/src/assets/emissary/concussion.png new file mode 100644 index 0000000..184aea4 Binary files /dev/null and b/src/renderer/src/assets/emissary/concussion.png differ diff --git a/src/renderer/src/assets/emissary/confident.png b/src/renderer/src/assets/emissary/confident.png new file mode 100644 index 0000000..5faf612 Binary files /dev/null and b/src/renderer/src/assets/emissary/confident.png differ diff --git a/src/renderer/src/assets/emissary/confused.png b/src/renderer/src/assets/emissary/confused.png new file mode 100644 index 0000000..115b681 Binary files /dev/null and b/src/renderer/src/assets/emissary/confused.png differ diff --git a/src/renderer/src/assets/emissary/crying.png b/src/renderer/src/assets/emissary/crying.png new file mode 100644 index 0000000..c720c63 Binary files /dev/null and b/src/renderer/src/assets/emissary/crying.png differ diff --git a/src/renderer/src/assets/emissary/flirting.png b/src/renderer/src/assets/emissary/flirting.png new file mode 100644 index 0000000..ef73990 Binary files /dev/null and b/src/renderer/src/assets/emissary/flirting.png differ diff --git a/src/renderer/src/assets/emissary/giggling.png b/src/renderer/src/assets/emissary/giggling.png new file mode 100644 index 0000000..72b8452 Binary files /dev/null and b/src/renderer/src/assets/emissary/giggling.png differ diff --git a/src/renderer/src/assets/emissary/hunger.png b/src/renderer/src/assets/emissary/hunger.png new file mode 100644 index 0000000..1806032 Binary files /dev/null and b/src/renderer/src/assets/emissary/hunger.png differ diff --git a/src/renderer/src/assets/emissary/idle-cheeky.png b/src/renderer/src/assets/emissary/idle-cheeky.png new file mode 100644 index 0000000..499368a Binary files /dev/null and b/src/renderer/src/assets/emissary/idle-cheeky.png differ diff --git a/src/renderer/src/assets/emissary/idle-sitting.png b/src/renderer/src/assets/emissary/idle-sitting.png new file mode 100644 index 0000000..485e73e Binary files /dev/null and b/src/renderer/src/assets/emissary/idle-sitting.png differ diff --git a/src/renderer/src/assets/emissary/idle.png b/src/renderer/src/assets/emissary/idle.png new file mode 100644 index 0000000..bccd1a1 Binary files /dev/null and b/src/renderer/src/assets/emissary/idle.png differ diff --git a/src/renderer/src/assets/emissary/joy.png b/src/renderer/src/assets/emissary/joy.png new file mode 100644 index 0000000..4389c14 Binary files /dev/null and b/src/renderer/src/assets/emissary/joy.png differ diff --git a/src/renderer/src/assets/emissary/keen.png b/src/renderer/src/assets/emissary/keen.png new file mode 100644 index 0000000..c9e5df6 Binary files /dev/null and b/src/renderer/src/assets/emissary/keen.png differ diff --git a/src/renderer/src/assets/emissary/lonely.png b/src/renderer/src/assets/emissary/lonely.png new file mode 100644 index 0000000..612defd Binary files /dev/null and b/src/renderer/src/assets/emissary/lonely.png differ diff --git a/src/renderer/src/assets/emissary/romantic-interest.png b/src/renderer/src/assets/emissary/romantic-interest.png new file mode 100644 index 0000000..295e2e0 Binary files /dev/null and b/src/renderer/src/assets/emissary/romantic-interest.png differ diff --git a/src/renderer/src/assets/emissary/romantic.png b/src/renderer/src/assets/emissary/romantic.png new file mode 100644 index 0000000..74de9af Binary files /dev/null and b/src/renderer/src/assets/emissary/romantic.png differ diff --git a/src/renderer/src/assets/emissary/sad-1.png b/src/renderer/src/assets/emissary/sad-1.png new file mode 100644 index 0000000..6bc857b Binary files /dev/null and b/src/renderer/src/assets/emissary/sad-1.png differ diff --git a/src/renderer/src/assets/emissary/sad-2.png b/src/renderer/src/assets/emissary/sad-2.png new file mode 100644 index 0000000..50ce4e6 Binary files /dev/null and b/src/renderer/src/assets/emissary/sad-2.png differ diff --git a/src/renderer/src/assets/emissary/searching.png b/src/renderer/src/assets/emissary/searching.png new file mode 100644 index 0000000..944a9b0 Binary files /dev/null and b/src/renderer/src/assets/emissary/searching.png differ diff --git a/src/renderer/src/assets/emissary/secretive.png b/src/renderer/src/assets/emissary/secretive.png new file mode 100644 index 0000000..52008aa Binary files /dev/null and b/src/renderer/src/assets/emissary/secretive.png differ diff --git a/src/renderer/src/assets/emissary/shock.png b/src/renderer/src/assets/emissary/shock.png new file mode 100644 index 0000000..45cf6b2 Binary files /dev/null and b/src/renderer/src/assets/emissary/shock.png differ diff --git a/src/renderer/src/assets/emissary/smiling.png b/src/renderer/src/assets/emissary/smiling.png new file mode 100644 index 0000000..cd9bce4 Binary files /dev/null and b/src/renderer/src/assets/emissary/smiling.png differ diff --git a/src/renderer/src/assets/emissary/smirks.png b/src/renderer/src/assets/emissary/smirks.png new file mode 100644 index 0000000..46f7a95 Binary files /dev/null and b/src/renderer/src/assets/emissary/smirks.png differ diff --git a/src/renderer/src/assets/emissary/speed.png b/src/renderer/src/assets/emissary/speed.png new file mode 100644 index 0000000..f1d9ff2 Binary files /dev/null and b/src/renderer/src/assets/emissary/speed.png differ diff --git a/src/renderer/src/assets/emissary/super-playful.png b/src/renderer/src/assets/emissary/super-playful.png new file mode 100644 index 0000000..c587133 Binary files /dev/null and b/src/renderer/src/assets/emissary/super-playful.png differ diff --git a/src/renderer/src/assets/emissary/swimming.png b/src/renderer/src/assets/emissary/swimming.png new file mode 100644 index 0000000..d81ae59 Binary files /dev/null and b/src/renderer/src/assets/emissary/swimming.png differ diff --git a/src/renderer/src/assets/emissary/teasing.png b/src/renderer/src/assets/emissary/teasing.png new file mode 100644 index 0000000..dae6e91 Binary files /dev/null and b/src/renderer/src/assets/emissary/teasing.png differ diff --git a/src/renderer/src/assets/emissary/very-confused.png b/src/renderer/src/assets/emissary/very-confused.png new file mode 100644 index 0000000..d5b2b77 Binary files /dev/null and b/src/renderer/src/assets/emissary/very-confused.png differ diff --git a/src/renderer/src/assets/emissary/waving-hello.png b/src/renderer/src/assets/emissary/waving-hello.png new file mode 100644 index 0000000..49d97fd Binary files /dev/null and b/src/renderer/src/assets/emissary/waving-hello.png differ diff --git a/src/renderer/src/assets/emissary/yearning.png b/src/renderer/src/assets/emissary/yearning.png new file mode 100644 index 0000000..ccbf7ab Binary files /dev/null and b/src/renderer/src/assets/emissary/yearning.png differ diff --git a/src/renderer/src/components/EmissaryFigure.tsx b/src/renderer/src/components/EmissaryFigure.tsx new file mode 100644 index 0000000..59963b4 --- /dev/null +++ b/src/renderer/src/components/EmissaryFigure.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import type { EmissaryAnimation, EmissaryEmotion } from '../screens/fishtank-lib' +import { ANIMATION_LABELS } from '../screens/fishtank-lib' +import type { ServiceStatus } from '../../../shared/types' + +// Dynamically import all portrait PNGs from the emissary assets folder. +// Vite resolves these at build time — no manual list needed as portraits grow. +const _emotionModules = import.meta.glob('../assets/emissary/*.png', { + eager: true, + import: 'default', +}) as Record + +const EMOTION_IMAGES: Record = Object.fromEntries( + Object.entries(_emotionModules).map(([path, url]) => { + const key = path.split('/').pop()!.replace('.png', '') + return [key, url] + }), +) + +function getEmotionImage(emotion: EmissaryEmotion): string { + return EMOTION_IMAGES[emotion] ?? EMOTION_IMAGES['idle'] ?? '' +} + +interface EmissaryFigureProps { + emotion: EmissaryEmotion + animation: EmissaryAnimation + status: ServiceStatus + name: string + style?: React.CSSProperties + onClick?: () => void + onKeyDown?: (e: React.KeyboardEvent) => void +} + +export const EmissaryFigure: React.FC = ({ + emotion, + animation, + status, + name, + style, + onClick, + onKeyDown, +}) => { + const src = getEmotionImage(emotion) + const haloGlow = status === 'running' + ? '0 0 60px rgba(0,200,212,0.35), 0 0 120px rgba(0,200,212,0.15)' + : '0 0 30px rgba(0,200,212,0.18), 0 0 60px rgba(0,200,212,0.07)' + + return ( +
+ {/* Glow halo ring sits behind the character */} +
+ + {/* Merman portrait PNG */} + {`${name} + + {/* Action label */} +
+ {ANIMATION_LABELS[animation]} +
+
+ ) +} diff --git a/src/renderer/src/index.css b/src/renderer/src/index.css index ec3117f..99b7f6d 100644 --- a/src/renderer/src/index.css +++ b/src/renderer/src/index.css @@ -519,17 +519,42 @@ main > div { cursor: pointer; } -.fishtank-emissary-orb { - width: 130px; - height: 130px; +/* Glow halo — radial soft ring that sits behind the PNG portrait */ +.fishtank-emissary-halo { + position: absolute; + width: 220px; + height: 220px; border-radius: 50%; - background: radial-gradient(circle at 40% 35%, rgba(0,200,212,0.15), rgba(0,200,212,0.03)); - border: 2px solid rgba(0,200,212,0.25); - display: flex; - align-items: center; - justify-content: center; - font-size: 60px; + background: radial-gradient(circle, rgba(0,200,212,0.12) 0%, transparent 70%); + border: 1.5px solid rgba(0,200,212,0.18); + top: 50%; + left: 50%; + transform: translate(-50%, -55%); + pointer-events: none; transition: box-shadow 1s; + animation: haloBreath 4s ease-in-out infinite; +} + +@keyframes haloBreath { + 0%, 100% { opacity: 0.8; transform: translate(-50%, -55%) scale(1); } + 50% { opacity: 1; transform: translate(-50%, -55%) scale(1.05); } +} + +/* The merman portrait PNG */ +.fishtank-emissary-img { + width: 260px; + height: 310px; + object-fit: contain; + object-position: center bottom; + filter: drop-shadow(0 0 18px rgba(0,200,212,0.35)) drop-shadow(0 0 6px rgba(0,200,212,0.5)); + position: relative; + z-index: 2; + transition: filter 0.8s ease, transform 0.3s ease; +} + +.fishtank-emissary:hover .fishtank-emissary-img { + filter: drop-shadow(0 0 28px rgba(0,200,212,0.5)) drop-shadow(0 0 10px rgba(0,200,212,0.6)); + transform: scale(1.03); } .fishtank-emissary-label { @@ -540,6 +565,8 @@ main > div { font-weight: 500; text-align: center; max-width: 200px; + position: relative; + z-index: 2; } .fishtank-speech { diff --git a/src/renderer/src/screens/Fishtank.tsx b/src/renderer/src/screens/Fishtank.tsx index 19abd8c..84b508b 100644 --- a/src/renderer/src/screens/Fishtank.tsx +++ b/src/renderer/src/screens/Fishtank.tsx @@ -6,6 +6,7 @@ import { IPC_CHANNELS } from '../../../shared/ipc' import { SeabedCanvas } from '../components/SeabedCanvas' import { HabitSuggestion } from '../components/HabitSuggestion' import { Tooltip } from '../components/Tooltip' +import { EmissaryFigure } from '../components/EmissaryFigure' import { type Weather, type EmissaryAnimation, @@ -16,8 +17,8 @@ import { WEATHER_CAUSTIC_OPACITY, ANIMATIONS, ANIMATION_LABELS, - ANIMATION_EMOJIS, getEmissaryStyle, + getEmissaryEmotion, getSayings, getStatusText, getClickResponse, @@ -251,6 +252,7 @@ export const Fishtank: React.FC = ({ status, recentTasks = [], on }, []) const emissaryStyle = getEmissaryStyle(animation, status) + const emissaryEmotion = getEmissaryEmotion(animation, status, weather) return (
@@ -392,27 +394,16 @@ export const Fishtank: React.FC = ({ status, recentTasks = [], on {/* Procedural seabed */} - {/* The Emissary — clickable for interaction */} -
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleEmissaryClick() } }} - className="fishtank-emissary" - style={emissaryStyle} - > -
- {ANIMATION_EMOJIS[animation]} -
-
- {ANIMATION_LABELS[animation]} -
-
+ /> {/* Speech bubble */}
diff --git a/src/renderer/src/screens/fishtank-lib/emissary.ts b/src/renderer/src/screens/fishtank-lib/emissary.ts index 11a3e95..6a7e77b 100644 --- a/src/renderer/src/screens/fishtank-lib/emissary.ts +++ b/src/renderer/src/screens/fishtank-lib/emissary.ts @@ -1,4 +1,5 @@ import type { ServiceStatus } from '../../../../shared/types' +import type { Weather } from './weather' // --- Expanded idle animations --- export type EmissaryAnimation = @@ -99,3 +100,94 @@ export function getEmissaryStyle(animation: EmissaryAnimation, status: ServiceSt } return { ...base, animation: map[animation] || 'emissaryFloat 4s ease-in-out infinite' } } + +// ── Emotion mapping for PNG sprite selection ────────────────────────────────── + +export type EmissaryEmotion = + | 'angry' + | 'ashamed' + | 'awaiting' + | 'blowing-bubbles' + | 'bored' + | 'broody' + | 'browsing' + | 'cheeky-2' + | 'cheeky-grin' + | 'cheeky' + | 'chortle' + | 'chuckling' + | 'concussion' + | 'confident' + | 'confused' + | 'crying' + | 'flirting' + | 'giggling' + | 'hunger' + | 'idle-cheeky' + | 'idle-sitting' + | 'idle' + | 'joy' + | 'keen' + | 'lonely' + | 'romantic-interest' + | 'romantic' + | 'sad-1' + | 'sad-2' + | 'searching' + | 'secretive' + | 'shock' + | 'smiling' + | 'smirks' + | 'speed' + | 'super-playful' + | 'swimming' + | 'teasing' + | 'very-confused' + | 'waving-hello' + | 'yearning' + +/** + * Derives which portrait image to display based on the current animation, + * service status, and weather state. + * + * Priority (highest → lowest): + * 1. Error status → shock + * 2. Thunderstorm weather → shock + * 3. Running status → keen + * 4. Golden weather → joy + * 5. Animation-specific portrait + * 6. Default → idle + */ +export function getEmissaryEmotion( + animation: EmissaryAnimation, + status: ServiceStatus, + weather: Weather, +): EmissaryEmotion { + if (status === 'error') return 'shock' + if (weather === 'thunderstorm') return 'shock' + if (status === 'running') return 'keen' + if (weather === 'golden') return 'joy' + + const map: Record = { + floating: 'idle', + swimming: 'swimming', + thinking: 'broody', + 'examining-scroll': 'searching', + waving: 'waving-hello', + stretching: 'idle-sitting', + gazing: 'yearning', + flirty: 'flirting', + fearful: 'shock', + preening: 'smirks', + 'collecting-pearls':'keen', + 'chasing-fish': 'super-playful', + flexing: 'confident', + singing: 'chortle', + napping: 'idle-sitting', + beckoning: 'romantic', + meditating: 'awaiting', + 'juggling-shells': 'cheeky', + posing: 'smirks', + } + return map[animation] ?? 'idle' +} diff --git a/src/renderer/src/screens/fishtank-lib/index.ts b/src/renderer/src/screens/fishtank-lib/index.ts index 587156b..fe0704a 100644 --- a/src/renderer/src/screens/fishtank-lib/index.ts +++ b/src/renderer/src/screens/fishtank-lib/index.ts @@ -1,10 +1,12 @@ export { type Weather, deriveWeather, WEATHER_BACKGROUNDS, WEATHER_CAUSTIC_OPACITY } from './weather' export { type EmissaryAnimation, + type EmissaryEmotion, ANIMATIONS, ANIMATION_LABELS, ANIMATION_EMOJIS, getEmissaryStyle, + getEmissaryEmotion, } from './emissary' export { getSayings, getStatusText, getClickResponse } from './sayings' export { diff --git a/src/renderer/src/vite-env.d.ts b/src/renderer/src/vite-env.d.ts new file mode 100644 index 0000000..3f4cc9d --- /dev/null +++ b/src/renderer/src/vite-env.d.ts @@ -0,0 +1,16 @@ +/// + +declare module '*.png' { + const src: string + export default src +} + +declare module '*.jpg' { + const src: string + export default src +} + +declare module '*.svg' { + const src: string + export default src +}