From eca259281a51edc858cdc18e66826617b6168eb2 Mon Sep 17 00:00:00 2001 From: Koala <6156589+Shik3i@users.noreply.github.com> Date: Thu, 28 May 2026 03:35:17 +0200 Subject: [PATCH] feat: expand emoji/name system with comprehensive animal mapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create shared/names.js as single source of truth for emoji maps, noun lists, adjectives, and username generation (215 animal emoji entries) - Remove duplicated inline lists from popup.js and background.js; both now import getAvatarForName / generateUsername from shared/names.js - Add names.js to the build script sync list so it stays DRY across extension builds - Fix missing cat emoji: CoolCat now correctly resolves to ðŸą - Sort emoji map keys by length at lookup time to prevent substring false-matches (e.g. 'caterpillar' before 'cat') --- extension/background.js | 47 +++++-- extension/popup.js | 23 +-- scripts/build-extension.js | 4 +- shared/names.js | 281 +++++++++++++++++++++++++++++++++++++ 4 files changed, 323 insertions(+), 32 deletions(-) create mode 100644 shared/names.js diff --git a/extension/background.js b/extension/background.js index 07ae98c..a073665 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,4 +1,5 @@ import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, APP_VERSION, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js'; +import { generateUsername } from './shared/names.js'; // --- State Management --- let socket = null; @@ -15,6 +16,8 @@ let pendingHistory = []; let eventQueue = []; let isNamespaceJoined = false; let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] }; +let localSeq = 0; // Monotonically increasing command sequence for this peer +const lastSeqBySender = {}; // senderId → last received seq (stale command guard) // --- Boot Sequence Lock --- let restorationTask = null; @@ -35,7 +38,7 @@ function ensureState() { 'logs', 'history', 'currentRoom', 'lastActionState', 'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks', 'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'currentTabId', 'currentTabTitle', - 'episodeLobby' + 'episodeLobby', 'localSeq' ], (data) => { clearTimeout(storageTimeout); if (data.currentTabId !== undefined) currentTabId = data.currentTabId; @@ -84,6 +87,8 @@ function ensureState() { } } + if (data.localSeq !== undefined && !isNaN(data.localSeq)) localSeq = data.localSeq; + storageInitialized = true; // Process any early logs/history that weren't captured in the spread @@ -186,9 +191,7 @@ async function getSettings() { } let username = data.username; if (!username) { - const adjs = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', 'Wild', 'Chill', 'Lucky', 'Epic', 'Swift', 'Bold', 'Mighty', 'Cosmic', 'Neon', 'Shadow', 'Crystal', 'Thunder', 'Silent', 'Golden', 'Fierce', 'Noble', 'Mystic', 'Frozen', 'Blazing', 'Sapphire', 'Iron', 'Crimson']; - const nouns = ['Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion', 'Hawk', 'Seal', 'Owl', 'Shark', 'Dragon', 'Phoenix', 'Falcon', 'Panther', 'Raven', 'Cobra', 'Lynx', 'Jaguar', 'Orca', 'Mantis', 'Viper', 'Condor', 'Badger', 'Otter', 'Rhino', 'Crane', 'Mongoose', 'Specter']; - username = `${adjs[Math.floor(Math.random() * adjs.length)]}${nouns[Math.floor(Math.random() * nouns.length)]}`; + username = generateUsername(); chrome.storage.sync.set({ username }, () => { resolve({ serverUrl: data.serverUrl || '', @@ -576,6 +579,14 @@ function handleServerEvent(event, data) { case EVENTS.PAUSE: case EVENTS.SEEK: case EVENTS.FORCE_SYNC_PREPARE: + if (data.senderId && typeof data.seq === 'number') { + const lastSeq = lastSeqBySender[data.senderId]; + if (lastSeq !== undefined && data.seq <= lastSeq) { + addLog(`Ignored stale ${event} from ${data.senderId} (seq ${data.seq} <= ${lastSeq})`, 'warn'); + break; + } + lastSeqBySender[data.senderId] = data.seq; + } if (data.senderId) { addToHistory(event, data.senderId); showNotification(data.senderId, event); @@ -592,6 +603,11 @@ function handleServerEvent(event, data) { routeToContent(event, data); break; case EVENTS.FORCE_SYNC_ACK: + if (data.senderId && typeof data.seq === 'number') { + const lastSeq = lastSeqBySender[data.senderId]; + if (lastSeq !== undefined && data.seq <= lastSeq) break; + lastSeqBySender[data.senderId] = data.seq; + } if (isForceSyncInitiator) { forceSyncAcks.add(data.senderId); chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) }); @@ -621,6 +637,11 @@ function handleServerEvent(event, data) { } break; case EVENTS.FORCE_SYNC_EXECUTE: + if (data?.senderId && typeof data.seq === 'number') { + const lastSeq = lastSeqBySender[data.senderId]; + if (lastSeq !== undefined && data.seq <= lastSeq) break; + lastSeqBySender[data.senderId] = data.seq; + } if (data?.senderId) { addToHistory(event, data.senderId); showNotification(data.senderId, event); @@ -701,7 +722,7 @@ function handleServerEvent(event, data) { peer.muted = data.muted !== undefined ? data.muted : peer.muted; const timeSinceReactive = peer.lastReactiveUpdate ? (Date.now() - peer.lastReactiveUpdate) : Infinity; - const ignoreStatus = timeSinceReactive < 1000; + const ignoreStatus = timeSinceReactive < 300; if (!ignoreStatus) { peer.playbackState = data.playbackState !== undefined ? data.playbackState : peer.playbackState; @@ -797,8 +818,11 @@ function executeForceSync() { const executionTimestamp = Date.now(); updateLastAction(EVENTS.FORCE_SYNC_EXECUTE, 'You', executionTimestamp); - emit(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp }); - routeToContent(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp }); + localSeq++; + chrome.storage.session.set({ localSeq }); + + emit(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp, seq: localSeq }); + routeToContent(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp, seq: localSeq }); addLog('Force Sync Executed', 'success'); } @@ -863,8 +887,10 @@ function executeEpisodeLobby() { }); const syncPayload = { targetTime: 0.0 }; - emit(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, peerId, actionTimestamp: timestamp }); - routeToContent(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, actionTimestamp: timestamp }); + localSeq++; + chrome.storage.session.set({ localSeq }); + emit(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, peerId, actionTimestamp: timestamp, seq: localSeq }); + routeToContent(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, actionTimestamp: timestamp, seq: localSeq }); forceSyncTimeout = setTimeout(() => { if (isForceSyncInitiator) { @@ -1137,10 +1163,13 @@ async function handleAsyncMessage(message, sender, sendResponse) { } else if (message.type === 'CONTENT_EVENT') { const processEvent = () => { const timestamp = Date.now(); + localSeq++; + chrome.storage.session.set({ localSeq }); updateLastAction(message.action, 'You', timestamp); lastActionState.targetTime = message.payload?.targetTime !== undefined ? message.payload.targetTime : message.payload?.currentTime; if (storageInitialized) chrome.storage.session.set({ lastActionState }); message.payload.actionTimestamp = timestamp; + message.payload.seq = localSeq; // Local Reactive Update updateLocalPeerState(peerId, { diff --git a/extension/popup.js b/extension/popup.js index 61386cc..5eaa412 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -1,5 +1,6 @@ import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js'; import { BLACKLIST_DOMAINS } from './shared/blacklist.js'; +import { getAvatarForName, generateUsername } from './shared/names.js'; const elements = { @@ -63,24 +64,6 @@ let errorToken = 0; let forceSyncDone = false; // --- Helpers --- -function getAvatarForName(username) { - if (!username) return 'ðŸ‘Ī'; - const lower = username.toLowerCase(); - const map = { - 'koala': 'ðŸĻ', 'panda': '🐞', 'tiger': 'ðŸŊ', 'eagle': 'ðŸĶ…', - 'fox': 'ðŸĶŠ', 'bear': 'ðŸŧ', 'wolf': '🐚', 'lion': 'ðŸĶ', - 'hawk': 'ðŸĶ…', 'seal': 'ðŸĶ­', 'owl': 'ðŸĶ‰', 'shark': 'ðŸĶˆ', - 'dragon': '🐉', 'phoenix': 'ðŸĶ', 'falcon': 'ðŸĶ…', 'panther': '🐆', - 'raven': 'ðŸĶ‍⮛', 'cobra': '🐍', 'lynx': '🐈', 'jaguar': '🐆', - 'orca': '🐋', 'mantis': 'ðŸĶ—', 'viper': '🐍', 'condor': 'ðŸĶ…', - 'badger': 'ðŸĶĄ', 'otter': 'ðŸĶĶ', 'rhino': 'ðŸĶ', 'crane': 'ðŸĶĐ', - 'mongoose': 'ðŸĶĶ', 'specter': 'ðŸ‘ŧ' - }; - for (const [key, emoji] of Object.entries(map)) { - if (lower.includes(key)) return emoji; - } - return 'ðŸ‘Ī'; -} // --- Initialization --- async function init() { @@ -88,9 +71,7 @@ async function init() { const data = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'filterNoise', 'username', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite']); let username = data.username; if (!username) { - const adjs = ['Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', 'Wild', 'Chill', 'Lucky', 'Epic', 'Swift', 'Bold', 'Mighty', 'Cosmic', 'Neon', 'Shadow', 'Crystal', 'Thunder', 'Silent', 'Golden', 'Fierce', 'Noble', 'Mystic', 'Frozen', 'Blazing', 'Sapphire', 'Iron', 'Crimson']; - const nouns = ['Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion', 'Hawk', 'Seal', 'Owl', 'Shark', 'Dragon', 'Phoenix', 'Falcon', 'Panther', 'Raven', 'Cobra', 'Lynx', 'Jaguar', 'Orca', 'Mantis', 'Viper', 'Condor', 'Badger', 'Otter', 'Rhino', 'Crane', 'Mongoose', 'Specter']; - username = `${adjs[Math.floor(Math.random() * adjs.length)]}${nouns[Math.floor(Math.random() * nouns.length)]}`; + username = generateUsername(); chrome.storage.sync.set({ username }); } diff --git a/scripts/build-extension.js b/scripts/build-extension.js index ea39d7c..31912eb 100644 --- a/scripts/build-extension.js +++ b/scripts/build-extension.js @@ -22,7 +22,7 @@ if (!fs.existsSync(extSharedDir)) { fs.mkdirSync(extSharedDir, { recursive: true }); } -const sharedFiles = ['constants.js', 'blacklist.js', 'README.md']; +const sharedFiles = ['constants.js', 'blacklist.js', 'names.js', 'README.md']; for (const file of sharedFiles) { const src = path.join(masterSharedDir, file); const dest = path.join(extSharedDir, file); @@ -31,7 +31,7 @@ for (const file of sharedFiles) { } fs.copyFileSync(src, dest); } -console.log('✓ constants.js, blacklist.js, and README.md synced to extension/shared/'); +console.log('✓ constants.js, blacklist.js, names.js, and README.md synced to extension/shared/'); // Read the base manifest const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8')); diff --git a/shared/names.js b/shared/names.js new file mode 100644 index 0000000..2c52b45 --- /dev/null +++ b/shared/names.js @@ -0,0 +1,281 @@ +/** + * KoalaSync Shared Name Generation & Emoji Mapping + * + * ⚠ïļ WARNING: This is the SINGLE SOURCE OF TRUTH. + * If you edit this file, you MUST run: node scripts/build-extension.js + * to propagate changes to the extension. + * + * The emoji map covers every animal/creature that has a Unicode emoji. + * Entries are sorted by key length (longest first) at lookup time to + * prevent substring false-matches (e.g. "caterpillar" must be checked + * before "cat"). + * + * If you add a new animal noun to USERNAME_NOUNS, ensure it has a + * corresponding entry in ANIMAL_EMOJI_MAP (or a substring that already + * maps to a suitable emoji). + */ + +export const USERNAME_ADJECTIVES = [ + 'Happy', 'Cool', 'Fast', 'Smart', 'Brave', 'Calm', 'Sneaky', 'Lazy', + 'Wild', 'Chill', 'Lucky', 'Epic', 'Swift', 'Bold', 'Mighty', + 'Cosmic', 'Neon', 'Shadow', 'Crystal', 'Thunder', 'Silent', 'Golden', + 'Fierce', 'Noble', 'Mystic', 'Frozen', 'Blazing', 'Sapphire', 'Iron', 'Crimson' +]; + +export const USERNAME_NOUNS = [ + 'Koala', 'Panda', 'Tiger', 'Eagle', 'Fox', 'Bear', 'Wolf', 'Lion', + 'Hawk', 'Seal', 'Owl', 'Shark', 'Dragon', 'Phoenix', 'Falcon', + 'Panther', 'Raven', 'Cobra', 'Lynx', 'Jaguar', 'Orca', 'Mantis', + 'Viper', 'Condor', 'Badger', 'Otter', 'Rhino', 'Crane', 'Mongoose', + 'Specter', + 'Cat', 'Dog', 'Deer', 'Bat', 'Gorilla', 'Monkey', 'Rabbit', + 'Horse', 'Unicorn', 'Zebra', 'Leopard', 'Cheetah', 'Puma', + 'Ram', 'Goat', 'Bull', 'Donkey', 'Moose', + 'Elephant', 'Giraffe', 'Hippo', 'Sloth', 'Kangaroo', + 'Raccoon', 'Hamster', 'Hedgehog', 'Skunk', 'Beaver', 'Bison', + 'Camel', 'Llama', 'Hyena', 'Coyote', + 'Mouse', 'Pig', 'Boar', 'Polar', 'Orangutan', 'Mammoth', + 'Crow', 'Duck', 'Swan', 'Penguin', 'Parrot', 'Peacock', + 'Dove', 'Dodo', 'Turkey', 'Flamingo', 'Chicken', 'Rooster', 'Goose', + 'Dolphin', 'Whale', 'Crab', 'Lobster', 'Octopus', 'Squid', + 'Jellyfish', 'Turtle', + 'Crocodile', 'Lizard', 'Snake', 'Frog', 'Toad', 'Gecko', + 'Bee', 'Ant', 'Spider', 'Scorpion', 'Butterfly', 'Ladybug', + 'Beetle', 'Snail', 'Dragonfly', 'Caterpillar', + 'Alien', 'Robot', 'Mermaid', 'Ghoul', 'Sprite', 'Cyborg', + 'Dinosaur', 'Reaper', 'Wraith', 'Sphinx', +]; + +export const ANIMAL_EMOJI_MAP = { + 'hippopotamus': 'ðŸĶ›', + 'rhinoceros': 'ðŸĶ', + 'caterpillar': '🐛', + 'chimpanzee': 'ðŸĩ', + 'orangutan': 'ðŸĶ§', + 'blackbird': 'ðŸĶ‍⮛', + 'bumblebee': '🐝', + 'cockatoo': 'ðŸĶœ', + 'cockroach': 'ðŸŠģ', + 'dragonfly': '🐉', + 'grasshopper': 'ðŸĶ—', + 'hedgehog': 'ðŸĶ”', + 'jellyfish': '🊞', + 'kangaroo': 'ðŸĶ˜', + 'ladybird': '🐞', + 'ladybug': '🐞', + 'porcupine': 'ðŸĶ”', + 'scorpion': 'ðŸĶ‚', + 'tarantula': '🕷ïļ', + 'alligator': '🐊', + 'anaconda': '🐍', + 'antelope': 'ðŸĶŒ', + 'blowfish': 'ðŸĄ', + 'butterfly': 'ðŸĶ‹', + 'chameleon': 'ðŸĶŽ', + 'chipmunk': 'ðŸŋïļ', + 'crocodile': '🐊', + 'dinosaur': 'ðŸĶ–', + 'elephant': '🐘', + 'flamingo': 'ðŸĶĐ', + 'giraffe': 'ðŸĶ’', + 'hamster': 'ðŸđ', + 'leopard': '🐆', + 'lobster': 'ðŸĶž', + 'mermaid': '🧜‍♀ïļ', + 'mongoose': 'ðŸĶĶ', + 'mosquito': 'ðŸĶŸ', + 'pangolin': 'ðŸĶ”', + 'peacock': 'ðŸĶš', + 'penguin': '🐧', + 'phoenix': 'ðŸĶ‍ðŸ”Ĩ', + 'raccoon': 'ðŸĶ', + 'seahorse': 'ðŸī', + 'sealion': 'ðŸĶ­', + 'unicorn': 'ðŸĶ„', + 'vampire': 'ðŸĶ‡', + 'warthog': '🐗', + 'wolverine': 'ðŸĶĄ', + 'mammoth': 'ðŸĶĢ', + 'meerkat': 'ðŸĶĶ', + 'octopus': '🐙', + 'opposum': '🐭', + 'ostrich': 'ðŸĶ', + 'panther': '🐆', + 'pelican': 'ðŸĶĐ', + 'rooster': '🐓', + 'serpent': '🐍', + 'specter': 'ðŸ‘ŧ', + 'spectre': 'ðŸ‘ŧ', + 'sparrow': 'ðŸĶ', + 'spider': '🕷ïļ', + 'sphinx': 'ðŸĶ', + 'squirrel': 'ðŸŋïļ', + 'stingray': 'ðŸĶˆ', + 'termite': '🐜', + 'tortoise': 'ðŸĒ', + 'turkey': 'ðŸĶƒ', + 'walrus': 'ðŸĶ­', + 'wombat': 'ðŸĶĄ', + 'woodpecker': 'ðŸĶ', + 'alien': 'ðŸ‘ū', + 'badger': 'ðŸĶĄ', + 'beaver': 'ðŸĶŦ', + 'beetle': 'ðŸŠē', + 'beluga': '🐋', + 'bison': 'ðŸĶŽ', + 'bobcat': 'ðŸą', + 'buffalo': 'ðŸĶŽ', + 'bunny': '🐰', + 'camel': '🐊', + 'cheetah': '🐆', + 'chicken': '🐔', + 'cobra': '🐍', + 'condor': 'ðŸĶ…', + 'cougar': '🐆', + 'coyote': '🐚', + 'crane': 'ðŸĶĐ', + 'cricket': 'ðŸĶ—', + 'crow': 'ðŸĶ‍⮛', + 'cyborg': 'ðŸĪ–', + 'dolphin': '🐎', + 'donkey': 'ðŸŦ', + 'dragon': '🐉', + 'drake': '🐉', + 'eagle': 'ðŸĶ…', + 'falcon': 'ðŸĶ…', + 'ferret': 'ðŸĶĶ', + 'gazelle': 'ðŸĶŒ', + 'gecko': 'ðŸĶŽ', + 'gerbil': 'ðŸđ', + 'ghost': 'ðŸ‘ŧ', + 'ghoul': 'ðŸ‘ŧ', + 'goose': 'ðŸŠŋ', + 'gopher': 'ðŸđ', + 'gorilla': 'ðŸĶ', + 'grizzly': 'ðŸŧ', + 'heron': 'ðŸĶĐ', + 'hippo': 'ðŸĶ›', + 'hornet': '🐝', + 'hyena': '🐚', + 'iguana': 'ðŸĶŽ', + 'jackal': '🐚', + 'jaguar': '🐆', + 'kitten': 'ðŸą', + 'koala': 'ðŸĻ', + 'lemur': '🐒', + 'lizard': 'ðŸĶŽ', + 'llama': 'ðŸĶ™', + 'locust': 'ðŸĶ—', + 'lynx': 'ðŸą', + 'macaw': 'ðŸĶœ', + 'mantis': 'ðŸĶ—', + 'mink': 'ðŸĶĶ', + 'monkey': 'ðŸĩ', + 'moose': 'ðŸĶŒ', + 'mouse': '🐭', + 'orca': '🐋', + 'otter': 'ðŸĶĶ', + 'oyster': 'ðŸĶŠ', + 'panda': '🐞', + 'parrot': 'ðŸĶœ', + 'pigeon': '🕊ïļ', + 'polar': 'ðŸŧ‍❄ïļ', + 'poodle': 'ðŸĐ', + 'puffin': '🐧', + 'puma': '🐆', + 'rabbit': '🐰', + 'raptor': 'ðŸĶ–', + 'raven': 'ðŸĶ‍⮛', + 'reaper': 'ðŸ‘ŧ', + 'rhino': 'ðŸĶ', + 'robin': 'ðŸĶ', + 'robot': 'ðŸĪ–', + 'salmon': '🐟', + 'shrimp': 'ðŸĶ', + 'skunk': 'ðŸĶĻ', + 'sloth': 'ðŸĶĨ', + 'snail': '🐌', + 'snake': '🐍', + 'sprite': '🧚', + 'squid': 'ðŸĶ‘', + 'swan': 'ðŸĶĒ', + 'tapir': '🐗', + 'tiger': 'ðŸŊ', + 'toad': 'ðŸļ', + 'trout': '🐟', + 'tuna': '🐟', + 'turtle': 'ðŸĒ', + 'viper': '🐍', + 'vulture': 'ðŸĶ…', + 'weasel': 'ðŸĶĶ', + 'whale': '🐋', + 'wolf': '🐚', + 'wraith': 'ðŸ‘ŧ', + 'zebra': 'ðŸĶ“', + 'ape': 'ðŸĶ', + 'ant': '🐜', + 'bat': 'ðŸĶ‡', + 'bee': '🐝', + 'bug': '🐛', + 'cat': 'ðŸą', + 'cow': 'ðŸŪ', + 'crab': 'ðŸĶ€', + 'dog': 'ðŸķ', + 'duck': 'ðŸĶ†', + 'elk': 'ðŸĶŒ', + 'fly': '🊰', + 'fox': 'ðŸĶŠ', + 'frog': 'ðŸļ', + 'goat': '🐐', + 'hawk': 'ðŸĶ…', + 'hen': '🐔', + 'hog': '🐷', + 'lion': 'ðŸĶ', + 'mole': '🐭', + 'moth': 'ðŸĶ‹', + 'mule': 'ðŸŦ', + 'owl': 'ðŸĶ‰', + 'pig': '🐷', + 'ram': '🐏', + 'rat': '🐀', + 'seal': 'ðŸĶ­', + 'shark': 'ðŸĶˆ', + 'wasp': '🐝', + 'yak': '🐂', + 'doe': 'ðŸĶŒ', + 'ewe': '🐑', + 'buck': 'ðŸĶŒ', + 'ox': '🐂', + 'bull': '🐂', + 'dodo': 'ðŸĶĪ', + 'boar': '🐗', + 'bear': 'ðŸŧ', + 'deer': 'ðŸĶŒ', + 'dove': '🕊ïļ', + 'fish': '🐟', + 'hare': '🐰', + 'horse': 'ðŸī', + 'lamb': '🐑', + 'mare': 'ðŸī', + 'pony': 'ðŸī', + 'pup': 'ðŸķ', + 'croc': '🐊', + 'gnat': 'ðŸĶŸ', + 'gnu': 'ðŸĶŽ', +}; + +export function getAvatarForName(username) { + if (!username) return '\u{1F464}'; + const lower = username.toLowerCase(); + const sorted = Object.entries(ANIMAL_EMOJI_MAP).sort((a, b) => b[0].length - a[0].length); + for (const [key, emoji] of sorted) { + if (lower.includes(key)) return emoji; + } + return '\u{1F464}'; +} + +export function generateUsername() { + const adj = USERNAME_ADJECTIVES[Math.floor(Math.random() * USERNAME_ADJECTIVES.length)]; + const noun = USERNAME_NOUNS[Math.floor(Math.random() * USERNAME_NOUNS.length)]; + return `${adj}${noun}`; +}