-
Notifications
You must be signed in to change notification settings - Fork 0
feat(assets): migrate 41 Azurel portraits from repo root into emissary asset tree #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
02f38ae
f4f3d65
d6b5aaa
712e7fd
98e4ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, string> | ||
|
|
||
| const EMOTION_IMAGES: Record<string, string> = 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<EmissaryFigureProps> = ({ | ||
| 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 ( | ||
| <div | ||
| onClick={onClick} | ||
| role="button" | ||
| tabIndex={0} | ||
| aria-label={`Click ${name} to interact`} | ||
| onKeyDown={onKeyDown} | ||
| className="fishtank-emissary" | ||
| style={style} | ||
| > | ||
| {/* Glow halo ring sits behind the character */} | ||
| <div | ||
| className="fishtank-emissary-halo" | ||
| style={{ boxShadow: haloGlow }} | ||
| /> | ||
|
|
||
| {/* Merman portrait PNG */} | ||
| <img | ||
| src={src} | ||
| alt={`${name} — ${emotion}`} | ||
| className="fishtank-emissary-img" | ||
| /> | ||
|
|
||
| {/* Action label */} | ||
| <div className="fishtank-emissary-label"> | ||
| {ANIMATION_LABELS[animation]} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<EmissaryAnimation, EmissaryEmotion> = { | ||
| floating: 'idle', | ||
| swimming: 'swimming', | ||
| thinking: 'broody', | ||
| 'examining-scroll': 'searching', | ||
| waving: 'waving-hello', | ||
| stretching: 'idle-sitting', | ||
| gazing: 'yearning', | ||
| flirty: 'flirting', | ||
| fearful: 'shock', | ||
|
Comment on lines
+176
to
+180
|
||
| 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' | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
|
Comment on lines
+4
to
+9
|
||
| } from './emissary' | ||
| export { getSayings, getStatusText, getClickResponse } from './sayings' | ||
| export { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| /// <reference types="vite/client" /> | ||
|
|
||
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EmissaryFigurerenders adivwithrole="button"/tabIndex={0}, but it doesn’t provide default keyboard activation behavior (Enter/Space) unless every caller remembers to pass anonKeyDownhandler. Since the component itself declares button semantics, consider handling Enter/Space internally (and still calling any passedonKeyDown) so the component is accessible by default.