@dotcraft/avatar 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +18 -20
  2. package/dist/AnimatedDecoration.d.ts +2 -2
  3. package/dist/AnimatedDecoration.d.ts.map +1 -1
  4. package/dist/AnimatedDecoration.js +2 -2
  5. package/dist/AnimatedDecoration.js.map +1 -1
  6. package/dist/AppearanceRig.d.ts +7 -1
  7. package/dist/AppearanceRig.d.ts.map +1 -1
  8. package/dist/AppearanceRig.js +2 -2
  9. package/dist/AppearanceRig.js.map +1 -1
  10. package/dist/Avatar.d.ts +2 -0
  11. package/dist/Avatar.d.ts.map +1 -1
  12. package/dist/Avatar.js +19 -7
  13. package/dist/Avatar.js.map +1 -1
  14. package/dist/BackDecorations.d.ts +9 -0
  15. package/dist/BackDecorations.d.ts.map +1 -0
  16. package/dist/BackDecorations.js +73 -0
  17. package/dist/BackDecorations.js.map +1 -0
  18. package/dist/DecorationShapes.d.ts +9 -2
  19. package/dist/DecorationShapes.d.ts.map +1 -1
  20. package/dist/DecorationShapes.js +8 -2
  21. package/dist/DecorationShapes.js.map +1 -1
  22. package/dist/Decorations.d.ts +6 -9
  23. package/dist/Decorations.d.ts.map +1 -1
  24. package/dist/Decorations.js +33 -16
  25. package/dist/Decorations.js.map +1 -1
  26. package/dist/FaceDecorations.d.ts +14 -0
  27. package/dist/FaceDecorations.d.ts.map +1 -0
  28. package/dist/FaceDecorations.js +69 -0
  29. package/dist/FaceDecorations.js.map +1 -0
  30. package/dist/HandDecorations.d.ts +5 -0
  31. package/dist/HandDecorations.d.ts.map +1 -0
  32. package/dist/HandDecorations.js +8 -0
  33. package/dist/HandDecorations.js.map +1 -0
  34. package/dist/HatDecorations.d.ts +2 -2
  35. package/dist/HatDecorations.d.ts.map +1 -1
  36. package/dist/HatDecorations.js +4 -0
  37. package/dist/HatDecorations.js.map +1 -1
  38. package/dist/MascotRig.d.ts +13 -1
  39. package/dist/MascotRig.d.ts.map +1 -1
  40. package/dist/MascotRig.js +6 -26
  41. package/dist/MascotRig.js.map +1 -1
  42. package/dist/ObjectDecorations.d.ts +2 -2
  43. package/dist/ObjectDecorations.d.ts.map +1 -1
  44. package/dist/ObjectDecorations.js +18 -2
  45. package/dist/ObjectDecorations.js.map +1 -1
  46. package/dist/RigMaterial.d.ts +18 -0
  47. package/dist/RigMaterial.d.ts.map +1 -0
  48. package/dist/RigMaterial.js +9 -0
  49. package/dist/RigMaterial.js.map +1 -0
  50. package/dist/SkinDecorations.d.ts +10 -0
  51. package/dist/SkinDecorations.d.ts.map +1 -0
  52. package/dist/SkinDecorations.js +28 -0
  53. package/dist/SkinDecorations.js.map +1 -0
  54. package/dist/accessories.css +1 -1
  55. package/dist/appearanceModel.d.ts +18 -14
  56. package/dist/appearanceModel.d.ts.map +1 -1
  57. package/dist/appearanceModel.js +62 -30
  58. package/dist/appearanceModel.js.map +1 -1
  59. package/dist/composer/styles/energy.css +34 -0
  60. package/dist/decorationCatalog.d.ts +17 -7
  61. package/dist/decorationCatalog.d.ts.map +1 -1
  62. package/dist/decorationCatalog.js +95 -36
  63. package/dist/decorationCatalog.js.map +1 -1
  64. package/dist/decorationMotion.d.ts +2 -2
  65. package/dist/decorationMotion.d.ts.map +1 -1
  66. package/dist/decorationMotion.js +4 -1
  67. package/dist/decorationMotion.js.map +1 -1
  68. package/dist/effects.css +86 -0
  69. package/dist/index.d.ts +2 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/items.d.ts +460 -0
  74. package/dist/items.d.ts.map +1 -0
  75. package/dist/items.js +52 -0
  76. package/dist/items.js.map +1 -0
  77. package/dist/paintMaterials.d.ts +31 -0
  78. package/dist/paintMaterials.d.ts.map +1 -0
  79. package/dist/paintMaterials.js +10 -0
  80. package/dist/paintMaterials.js.map +1 -0
  81. package/dist/palette.d.ts +2 -0
  82. package/dist/palette.d.ts.map +1 -1
  83. package/dist/palette.js +4 -4
  84. package/dist/palette.js.map +1 -1
  85. package/dist/react.d.ts +2 -2
  86. package/dist/react.d.ts.map +1 -1
  87. package/dist/react.js +2 -2
  88. package/dist/react.js.map +1 -1
  89. package/dist/rig.css +6 -21
  90. package/dist/styles.css +1 -0
  91. package/package.json +1 -1
  92. package/src/AnimatedDecoration.tsx +4 -4
  93. package/src/AppearanceRig.tsx +5 -4
  94. package/src/Avatar.tsx +25 -11
  95. package/src/BackDecorations.tsx +134 -0
  96. package/src/DecorationShapes.tsx +11 -3
  97. package/src/Decorations.tsx +47 -22
  98. package/src/FaceDecorations.tsx +145 -0
  99. package/src/HandDecorations.tsx +113 -0
  100. package/src/HatDecorations.tsx +29 -2
  101. package/src/MascotRig.tsx +27 -44
  102. package/src/ObjectDecorations.tsx +69 -3
  103. package/src/RigMaterial.tsx +23 -0
  104. package/src/SkinDecorations.tsx +56 -0
  105. package/src/accessories.css +1 -1
  106. package/src/appearanceModel.ts +57 -37
  107. package/src/composer/styles/energy.css +34 -0
  108. package/src/decorationCatalog.ts +103 -38
  109. package/src/decorationMotion.ts +6 -3
  110. package/src/effects.css +86 -0
  111. package/src/index.ts +2 -0
  112. package/src/items.ts +86 -0
  113. package/src/paintMaterials.ts +18 -0
  114. package/src/palette.ts +7 -3
  115. package/src/react.ts +2 -2
  116. package/src/rig.css +6 -21
  117. package/src/styles.css +1 -0
  118. package/dist/HeldDecorations.d.ts +0 -5
  119. package/dist/HeldDecorations.d.ts.map +0 -1
  120. package/dist/HeldDecorations.js +0 -5
  121. package/dist/HeldDecorations.js.map +0 -1
  122. package/src/HeldDecorations.tsx +0 -48
@@ -9,6 +9,6 @@
9
9
  .dca-robot[data-gesture='blink'] .dca-part-caret { animation: none !important; }
10
10
 
11
11
  /* Lift an upright tool beside the face rather than sweeping it across the screen. */
12
- .dca-robot[data-held-state='holding'] .dca-action-celebrate :is(.dca-part-arm-l-w, .dca-part-arm-l) {
12
+ .dca-robot[data-held-state='holding'] .dca-action-celebrate :is(.dca-part-arm-l-w, .dca-part-arm-l-b, .dca-part-arm-l) {
13
13
  transform: translate(30px, -65px) rotate(20deg);
14
14
  }
@@ -1,38 +1,20 @@
1
- // Frozen v1 draw order. IDs are serialized; positions are only sampling weights.
2
- export const primaryIds = ['none', 'baseball-cap', 'bucket-hat', 'beret', 'beanie', 'top-hat', 'wizard-hat', 'chef-hat', 'party-hat', 'crown', 'hard-hat', 'nightcap', 'straw-hat', 'poop', 'banana', 'fried-egg', 'rubber-duck', 'paper-boat', 'traffic-cone', 'sprout', 'donut'] as const
3
- export const heldIds = ['task-board', 'wrench', 'shield', 'magnifier', 'control-panel'] as const
4
- export type HeldId = typeof heldIds[number]
5
- export const secondaryIds = ['none', 'forehead-goggles', ...heldIds] as const
6
- export type PrimaryId = typeof primaryIds[number]
7
- export type SecondaryId = typeof secondaryIds[number]
8
- export type DecorationId = Exclude<PrimaryId | SecondaryId, 'none'>
1
+ import { conflicts, itemOf, items, rarities, slots, type BackId, type FaceId, type HandId, type HeadId, type ItemId, type ItemOf, type Rarity, type SkinId, type Slot, type Zone } from './items.js'
2
+
9
3
  export interface Appearance {
10
- version: 2
4
+ version: 1
11
5
  palette: number
12
6
  baseFace: number
13
- primary: PrimaryId
14
- secondary: SecondaryId
7
+ head: HeadId | 'none'
8
+ face: FaceId | 'none'
9
+ hand: HandId | 'none'
10
+ back: BackId | 'none'
11
+ skin: SkinId | 'none'
15
12
  }
13
+ export type SlotValue<S extends Slot> = ItemOf<S> | 'none'
14
+
15
+ export const slotPresence: Record<Slot, number> = { head: .85, face: .35, hand: .4, back: .25, skin: .3 }
16
+ export const rarityWeights: Record<Rarity, number> = { common: 55, uncommon: 27, rare: 12, epic: 5, legendary: 1 }
16
17
 
17
- // Explicit allow-list, including the undecorated head. No cartesian-product fallback.
18
- const faceAccessories = ['none', ...heldIds] as const
19
- export const compatibility: Record<PrimaryId, readonly SecondaryId[]> = {
20
- none: [...faceAccessories, 'forehead-goggles'],
21
- 'baseball-cap': faceAccessories, 'bucket-hat': faceAccessories, beret: faceAccessories,
22
- beanie: faceAccessories, 'top-hat': faceAccessories, 'wizard-hat': faceAccessories,
23
- 'chef-hat': faceAccessories, 'party-hat': faceAccessories, crown: faceAccessories,
24
- 'hard-hat': faceAccessories, nightcap: faceAccessories, 'straw-hat': faceAccessories,
25
- poop: [...faceAccessories, 'forehead-goggles'], banana: [...faceAccessories, 'forehead-goggles'],
26
- 'fried-egg': [...faceAccessories, 'forehead-goggles'], 'rubber-duck': [...faceAccessories, 'forehead-goggles'],
27
- 'paper-boat': [...faceAccessories, 'forehead-goggles'], 'traffic-cone': [...faceAccessories, 'forehead-goggles'],
28
- sprout: [...faceAccessories, 'forehead-goggles'], donut: [...faceAccessories, 'forehead-goggles'],
29
- }
30
- export function isCompatible(primary: PrimaryId, secondary: SecondaryId) {
31
- return compatibility[primary].includes(secondary)
32
- }
33
- export function withPrimary(appearance: Appearance, primary: PrimaryId): Appearance {
34
- return { ...appearance, primary, secondary: isCompatible(primary, appearance.secondary) ? appearance.secondary : 'none' }
35
- }
36
18
  export function hashSeed(seed: string): number {
37
19
  let value = 0x811c9dc5
38
20
  for (let index = 0; index < seed.length; index++) value = Math.imul(value ^ seed.charCodeAt(index), 0x01000193)
@@ -41,18 +23,56 @@ export function hashSeed(seed: string): number {
41
23
  value ^= value >>> 15; value = Math.imul(value, 0x846ca68b)
42
24
  return (value ^ (value >>> 16)) >>> 0
43
25
  }
44
- export function isHeld(id: SecondaryId): id is HeldId { return (heldIds as readonly string[]).includes(id) }
45
- export const originalAppearance: Appearance = { version: 2, palette: -1, baseFace: 0, primary: 'none', secondary: 'none' }
26
+ export const originalAppearance: Appearance = { version: 1, palette: -1, baseFace: 0, head: 'none', face: 'none', hand: 'none', back: 'none', skin: 'none' }
46
27
  export function normalizeName(name: string): string { return name.trim().normalize('NFC') }
28
+
29
+ export function equippedIds(appearance: Appearance): ItemId[] {
30
+ return slots.map(slot => appearance[slot]).filter((id): id is ItemId => id !== 'none')
31
+ }
32
+ export function occupiedZones(appearance: Appearance): Set<Zone> {
33
+ return new Set(equippedIds(appearance).flatMap(id => [...itemOf(id).zones]))
34
+ }
35
+ export function canEquip<S extends Slot>(appearance: Appearance, slot: S, id: SlotValue<S>): boolean {
36
+ if (id === 'none') return true
37
+ if (itemOf(id).slot !== slot) return false
38
+ return slots.every(other => other === slot || appearance[other] === 'none' || !conflicts(appearance[other] as ItemId, id))
39
+ }
40
+ export function equip<S extends Slot>(appearance: Appearance, slot: S, id: SlotValue<S>): { appearance: Appearance; cleared: Slot[] } {
41
+ if (id !== 'none' && itemOf(id).slot !== slot) throw new TypeError(`Cannot equip ${id} in the ${slot} slot`)
42
+ const next: Appearance = { ...appearance, [slot]: id }
43
+ const cleared: Slot[] = []
44
+ if (id !== 'none') for (const other of slots) {
45
+ const current = next[other]
46
+ if (other !== slot && current !== 'none' && conflicts(current, id)) { (next as Record<Slot, string>)[other] = 'none'; cleared.push(other) }
47
+ }
48
+ return { appearance: next, cleared }
49
+ }
50
+ export function hasConflicts(appearance: Appearance): boolean {
51
+ const ids = equippedIds(appearance)
52
+ return ids.some((a, index) => ids.slice(index + 1).some(b => conflicts(a, b)))
53
+ }
54
+
47
55
  export function deriveAppearance(name: string): Appearance {
48
56
  const seed = normalizeName(name)
49
57
  if (!seed) return { ...originalAppearance }
50
58
  const draw = (dimension: string) => hashSeed(JSON.stringify(['dotcraft-avatar', 1, seed, dimension])) / 0x100000000
51
- const primary = primaryIds[Math.floor(draw('primary') * primaryIds.length)]
52
- const choices = compatibility[primary].filter(id => id !== 'none')
53
- const secondaryDraw = (dimension: string) => hashSeed(JSON.stringify(['dotcraft-avatar', 2, seed, dimension])) / 0x100000000
54
- const secondary = secondaryDraw('secondary-presence') < .5 ? 'none' : choices[Math.floor(secondaryDraw('secondary-item') * choices.length)]
55
- return { version: 2, palette: Math.floor(draw('palette') * 12), baseFace: Math.floor(draw('face') * 5), primary, secondary }
59
+ const appearance: Appearance = { ...originalAppearance, palette: Math.floor(draw('palette') * 12), baseFace: Math.floor(draw('face') * 5) }
60
+ const occupied = new Set<Zone>()
61
+ for (const slot of slots) {
62
+ if (draw(`${slot}-presence`) >= slotPresence[slot]) continue
63
+ const slotItems = items.filter(item => item.slot === slot)
64
+ const tiers = rarities.filter(rarity => slotItems.some(item => item.rarity === rarity))
65
+ let roll = draw(`${slot}-rarity`) * tiers.reduce((sum, rarity) => sum + rarityWeights[rarity], 0)
66
+ let rarity: Rarity = tiers[tiers.length - 1]
67
+ for (const tier of tiers) { roll -= rarityWeights[tier]; if (roll < 0) { rarity = tier; break } }
68
+ // A tier with no zone-compatible item leaves the slot empty, so rare combinations stay rare.
69
+ const pool = slotItems.filter(item => item.rarity === rarity && item.zones.every(zone => !occupied.has(zone)))
70
+ if (!pool.length) continue
71
+ const pick = pool[Math.floor(draw(`${slot}-item`) * pool.length)]
72
+ ;(appearance as Record<Slot, string>)[slot] = pick.id
73
+ for (const zone of pick.zones) occupied.add(zone)
74
+ }
75
+ return appearance
56
76
  }
57
77
  export function sampleSeed(seed: string, round: number, index: number) {
58
78
  return JSON.stringify([seed, round, index])
@@ -72,11 +72,45 @@
72
72
  calc(h + 27.55)) 36%, transparent));
73
73
  }
74
74
 
75
+ @keyframes dca-skin-energy {
76
+ 0%, 100% { opacity: 0; }
77
+ 50% { opacity: var(--dca-skin-energy-peak); }
78
+ }
79
+
80
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-skin-energy {
81
+ --dca-skin-energy-peak: .16;
82
+ animation: dca-skin-energy 6s ease-in-out infinite;
83
+ }
84
+
85
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-skin-energy {
86
+ --dca-skin-energy-peak: .26;
87
+ animation: dca-skin-energy 3.8s ease-in-out infinite;
88
+ }
89
+
90
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-skin-energy {
91
+ --dca-skin-energy-peak: .38;
92
+ animation: dca-skin-energy 2.8s ease-in-out infinite;
93
+ }
94
+
95
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 4.6s; }
96
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 3.2s; }
97
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy, .dca-skin-holo) stop { animation-duration: 2.2s; }
98
+ [data-mascot-effort='medium'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 3.4s; }
99
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 2.4s; }
100
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] .dca-fx-sheen { animation-duration: 1.6s; }
101
+ [data-mascot-effort='high'] .dca-robot[data-effects='live'] [data-skin-overlay='galaxy'] .dca-fx-node { animation-duration: 1.1s; }
102
+ [data-mascot-effort='extraHigh'] .dca-robot[data-effects='live'] [data-skin-overlay='galaxy'] .dca-fx-node { animation-duration: .7s; }
103
+
75
104
  [data-mascot-context='max'] .dca-robot {
76
105
  animation: composer-mascot-max-overdrive-edge 3.8s ease-in-out infinite;
77
106
  will-change: filter;
78
107
  }
79
108
 
109
+ [data-mascot-context='max'] .dca-robot[data-effects='live'] .dca-skin-energy {
110
+ --dca-skin-energy-peak: .3;
111
+ animation: dca-skin-energy 3.8s ease-in-out infinite;
112
+ }
113
+
80
114
  [data-mascot-context='max'] .dca-robot .dca-paint-body stop:nth-child(1) {
81
115
  animation: composer-mascot-max-coat-dark 3.8s ease-in-out infinite;
82
116
  }
@@ -1,45 +1,110 @@
1
- import { compatibility, primaryIds, type DecorationId, type PrimaryId, type SecondaryId } from './appearanceModel.js'
1
+ import { itemOf, items, rarities, seriesIds, slots, type Item, type ItemId, type Rarity, type Series, type Slot } from './items.js'
2
2
 
3
- export interface Decoration {
4
- id: DecorationId
3
+ export interface DecorationCopy {
5
4
  name: string
6
- category: 'Hats' | 'Novelty' | 'Accessories'
7
5
  feature: string
8
6
  colors: readonly string[]
9
7
  mount: string
10
8
  }
11
- export const decorations: Decoration[] = [
12
- { id: 'baseball-cap', name: 'Baseball cap', category: 'Hats', feature: 'Rounded crown with a short right-facing brim', colors: ['#e87967', '#b94f50', '#f5a18c'], mount: 'Crown overlaps the top edge; the brim stays inside the body' },
13
- { id: 'bucket-hat', name: 'Bucket hat', category: 'Hats', feature: 'Flat top with a continuous downward brim', colors: ['#94bda6', '#5b887a', '#bed8c1'], mount: 'Brim follows the top edge with a slight center overlap' },
14
- { id: 'beret', name: 'Beret', category: 'Hats', feature: 'Soft left-leaning crown with a short stem', colors: ['#b17498', '#784c71', '#cf9db5'], mount: 'Narrow band rests on top; the crown leans left' },
15
- { id: 'beanie', name: 'Beanie', category: 'Hats', feature: 'Rounded crown and wide folded cuff with minimal texture', colors: ['#e9a653', '#bc753d', '#f7c676'], mount: 'Folded cuff sits on the top edge; crown stays centered' },
16
- { id: 'top-hat', name: 'Top hat', category: 'Hats', feature: 'Tall crown, narrow brim, and burgundy band', colors: ['#465069', '#293247', '#c96e85'], mount: 'Narrow brim rests flat on top; crown tapers at its base' },
17
- { id: 'wizard-hat', name: 'Wizard hat', category: 'Hats', feature: 'Curved point, wide brim, and gold star', colors: ['#8773c1', '#594b93', '#f4cc75'], mount: 'Wide brim rests on top; point curves into the center safe area' },
18
- { id: 'chef-hat', name: 'Chef hat', category: 'Hats', feature: 'Three-lobed puff with an upright band', colors: ['#f5eee3', '#d7caba', '#fffaf1'], mount: 'Band rests at the center of the top edge' },
19
- { id: 'party-hat', name: 'Party hat', category: 'Hats', feature: 'Cone with two colored bands and a pom-pom', colors: ['#ea9baf', '#8075b7', '#f5d07b'], mount: 'Shallow curved base follows the top of the body' },
20
- { id: 'crown', name: 'Crown', category: 'Hats', feature: 'Three rounded points with a central sapphire', colors: ['#efc65c', '#c99139', '#73b8cc'], mount: 'Curved gold band wraps the top edge; all points face up' },
21
- { id: 'hard-hat', name: 'Hard hat', category: 'Hats', feature: 'Rounded shell, center ridge, and short front brim', colors: ['#f5bd48', '#d68d32', '#ffdc79'], mount: 'Short brim overlaps the top edge; ridge stays centered' },
22
- { id: 'nightcap', name: 'Nightcap', category: 'Hats', feature: 'Soft drooping point with a pom-pom', colors: ['#789cc9', '#4c709f', '#dce8f4'], mount: 'Soft band follows the top edge; pom-pom stays upper right' },
23
- { id: 'straw-hat', name: 'Straw hat', category: 'Hats', feature: 'Low round crown, wide oval brim, and red band', colors: ['#e8c78a', '#c49b5e', '#c57568'], mount: 'Wide brim rests flat without entering the arm areas' },
24
- { id: 'poop', name: 'Poop', category: 'Novelty', feature: 'Three rounded layers with a playful curled tip', colors: ['#a97b5f', '#805840', '#c49976'], mount: 'Wide bottom layer rests directly on top; color separates each layer' },
25
- { id: 'banana', name: 'Banana', category: 'Novelty', feature: 'Crescent fruit with stems and an inner color plane', colors: ['#f3cf62', '#dbaa40', '#89705a'], mount: 'Outer curve touches the top; both stems point up' },
26
- { id: 'fried-egg', name: 'Fried egg', category: 'Novelty', feature: 'Irregular rounded white with a raised golden yolk', colors: ['#fff9e8', '#e6d9bb', '#f3be43'], mount: 'White spreads across the top; raised yolk defines the silhouette' },
27
- { id: 'rubber-duck', name: 'Rubber duck', category: 'Novelty', feature: 'Round head, flat orange bill, and short raised tail', colors: ['#f6d05e', '#e4ae3e', '#e58b4b'], mount: 'Belly rests flat on top, with bill left and tail right' },
28
- { id: 'paper-boat', name: 'Paper boat', category: 'Novelty', feature: 'Folded hull with a central triangular plane', colors: ['#d6e7eb', '#92b5c1', '#f1f7f5'], mount: 'Folded hull edge rests on top; main fold stays centered' },
29
- { id: 'traffic-cone', name: 'Traffic cone', category: 'Novelty', feature: 'Orange cone with a white band and flat base', colors: ['#ed985f', '#c96c43', '#fff0dd'], mount: 'Flat base rests on top; cone tapers upward' },
30
- { id: 'sprout', name: 'Sprout', category: 'Novelty', feature: 'Short stem with two leaves pointing in different directions', colors: ['#89b875', '#537f59', '#b5d38f'], mount: 'Stem grows directly from the top edge without a pot or base' },
31
- { id: 'donut', name: 'Donut', category: 'Novelty', feature: 'Clear center hole, half-ring of icing, and a few sprinkles', colors: ['#dbab70', '#bf8456', '#e5a0b3'], mount: 'Lower curve touches the top; center remains an open cutout' },
32
- { id: 'forehead-goggles', name: 'Forehead goggles', category: 'Accessories', feature: 'Two blue-gray lenses with a center bridge', colors: ['#8b7568', '#a2c5d1', '#dec8ae'], mount: 'Fits within the forehead band; bridge anchors above the screen' },
33
- {"id":"task-board","name":"Task board","category":"Accessories","feature":"White planning board with connected role-color markers and a yellow accent","colors":["#ffffff","#3161f7","#f6b500"],"mount":"Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores"},
34
- {"id":"wrench","name":"Wrench","category":"Accessories","feature":"White open-ended wrench with role-color outlines and a yellow handle inset","colors":["#ffffff","#3161f7","#f6b500"],"mount":"Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores"},
35
- {"id":"shield","name":"Shield","category":"Accessories","feature":"White shield with a role-color outline and upright check","colors":["#ffffff","#3161f7","#f6b500"],"mount":"Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores"},
36
- {"id":"magnifier","name":"Magnifier","category":"Accessories","feature":"Upright white lens frame with a role-color outline and a lower grip","colors":["#ffffff","#3161f7","#f6b500"],"mount":"Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores"},
37
- {"id":"control-panel","name":"Control panel","category":"Accessories","feature":"White control panel with dark rails and role-color and yellow knobs","colors":["#ffffff","#3161f7","#f6b500"],"mount":"Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores"},
38
- ]
39
- export const decorationOf = (id: DecorationId) => decorations.find(item => item.id === id)!
40
- export const decorationName = (id: PrimaryId | SecondaryId) => id === 'none' ? 'None' : decorationOf(id).name
41
- export function compatibleNames(item: Decoration) {
42
- return item.category === 'Accessories'
43
- ? primaryIds.filter(id => compatibility[id].includes(item.id as SecondaryId)).map(decorationName).join(', ')
44
- : compatibility[item.id as PrimaryId].map(decorationName).join(', ')
9
+ export type Decoration = Item & DecorationCopy
10
+
11
+ export const slotLabels: Record<Slot, string> = { head: 'Head', face: 'Face', hand: 'Hand', back: 'Back', skin: 'Skin' }
12
+ export const rarityMeta: Record<Rarity, { label: string; color: string }> = {
13
+ common: { label: 'Common', color: '#8a94a6' },
14
+ uncommon: { label: 'Uncommon', color: '#3f9d5b' },
15
+ rare: { label: 'Rare', color: '#3b7be8' },
16
+ epic: { label: 'Epic', color: '#8b5cf6' },
17
+ legendary: { label: 'Legendary', color: '#e0962c' },
18
+ }
19
+ export const seriesLabels: Record<Series, string> = {
20
+ everyday: 'Everyday', dev: 'Dev culture', tech: 'Tech & cyber', fantasy: 'Fantasy', nature: 'Nature', snack: 'Snack', critters: 'Critters',
45
21
  }
22
+
23
+ const held = 'Screen-left hand; shares the arm pivot, stows for laptop and question sign, then restores'
24
+ const copy: Record<ItemId, DecorationCopy> = {
25
+ 'baseball-cap': { name: 'Baseball cap', feature: 'Rounded crown with a short right-facing brim', colors: ['#e87967', '#b94f50', '#f5a18c'], mount: 'Crown overlaps the top edge; the brim stays inside the body' },
26
+ 'bucket-hat': { name: 'Bucket hat', feature: 'Flat top with a continuous downward brim', colors: ['#94bda6', '#5b887a', '#bed8c1'], mount: 'Brim follows the top edge with a slight center overlap' },
27
+ beret: { name: 'Beret', feature: 'Soft left-leaning crown with a short stem', colors: ['#b17498', '#784c71', '#cf9db5'], mount: 'Narrow band rests on top; the crown leans left' },
28
+ beanie: { name: 'Beanie', feature: 'Rounded crown and wide folded cuff with minimal texture', colors: ['#e9a653', '#bc753d', '#f7c676'], mount: 'Folded cuff sits on the top edge; crown stays centered' },
29
+ 'top-hat': { name: 'Top hat', feature: 'Tall crown, narrow brim, and burgundy band', colors: ['#465069', '#293247', '#c96e85'], mount: 'Narrow brim rests flat on top; crown tapers at its base' },
30
+ 'wizard-hat': { name: 'Wizard hat', feature: 'Curved point, wide brim, and gold star', colors: ['#8773c1', '#594b93', '#f4cc75'], mount: 'Wide brim rests on top; point curves into the center safe area' },
31
+ 'chef-hat': { name: 'Chef hat', feature: 'Three-lobed puff with an upright band', colors: ['#f5eee3', '#d7caba', '#fffaf1'], mount: 'Band rests at the center of the top edge' },
32
+ 'party-hat': { name: 'Party hat', feature: 'Cone with two colored bands and a pom-pom', colors: ['#ea9baf', '#8075b7', '#f5d07b'], mount: 'Shallow curved base follows the top of the body' },
33
+ crown: { name: 'Crown', feature: 'Three rounded points with a central sapphire', colors: ['#efc65c', '#c99139', '#73b8cc'], mount: 'Curved gold band wraps the top edge; all points face up' },
34
+ 'hard-hat': { name: 'Hard hat', feature: 'Rounded shell, center ridge, and short front brim', colors: ['#f5bd48', '#d68d32', '#ffdc79'], mount: 'Short brim overlaps the top edge; ridge stays centered' },
35
+ nightcap: { name: 'Nightcap', feature: 'Soft drooping point with a pom-pom', colors: ['#789cc9', '#4c709f', '#dce8f4'], mount: 'Soft band follows the top edge; pom-pom stays upper right' },
36
+ 'straw-hat': { name: 'Straw hat', feature: 'Low round crown, wide oval brim, and red band', colors: ['#e8c78a', '#c49b5e', '#c57568'], mount: 'Wide brim rests flat without entering the arm areas' },
37
+ poop: { name: 'Poop', feature: 'Three rounded layers with a playful curled tip', colors: ['#a97b5f', '#805840', '#c49976'], mount: 'Wide bottom layer rests directly on top; color separates each layer' },
38
+ banana: { name: 'Banana', feature: 'Crescent fruit with stems and an inner color plane', colors: ['#f3cf62', '#dbaa40', '#89705a'], mount: 'Outer curve touches the top; both stems point up' },
39
+ 'fried-egg': { name: 'Fried egg', feature: 'Irregular rounded white with a raised golden yolk', colors: ['#fff9e8', '#e6d9bb', '#f3be43'], mount: 'White spreads across the top; raised yolk defines the silhouette' },
40
+ 'rubber-duck': { name: 'Rubber duck', feature: 'Round head, flat orange bill, and short raised tail', colors: ['#f6d05e', '#e4ae3e', '#e58b4b'], mount: 'Belly rests flat on top, with bill left and tail right' },
41
+ 'paper-boat': { name: 'Paper boat', feature: 'Folded hull with a central triangular plane', colors: ['#d6e7eb', '#92b5c1', '#f1f7f5'], mount: 'Folded hull edge rests on top; main fold stays centered' },
42
+ 'traffic-cone': { name: 'Traffic cone', feature: 'Orange cone with a white band and flat base', colors: ['#ed985f', '#c96c43', '#fff0dd'], mount: 'Flat base rests on top; cone tapers upward' },
43
+ sprout: { name: 'Sprout', feature: 'Short stem with two leaves pointing in different directions', colors: ['#89b875', '#537f59', '#b5d38f'], mount: 'Stem grows directly from the top edge without a pot or base' },
44
+ donut: { name: 'Donut', feature: 'Clear center hole, half-ring of icing, and a few sprinkles', colors: ['#dbab70', '#bf8456', '#e5a0b3'], mount: 'Lower curve touches the top; center remains an open cutout' },
45
+ 'ringed-planet': { name: 'Ringed planet', feature: 'Banded sand-colored planet inside a tilted pale ring with a soft glow', colors: ['#d9b07a', '#b8875a', '#f1e3c8'], mount: 'Ring underside touches the top edge; the planet floats above the center' },
46
+ 'cat-ears': { name: 'Cat ears', feature: 'Two tan triangular ears with pink inner planes', colors: ['#c9a27e', '#f2a0b4'], mount: 'Ears rise from the top corners; nothing sits between them' },
47
+ 'cowboy-hat': { name: 'Cowboy hat', feature: 'Tall creased crown over a wide curved brim with a dark band', colors: ['#b98352', '#a8734a', '#6e4a2e'], mount: 'Brim rests across the top edge and stays clear of the arms' },
48
+ mushroom: { name: 'Mushroom', feature: 'Red cap with cream spots on a short cream stem', colors: ['#e8654f', '#fff4ef', '#fff1dc'], mount: 'Stem stands on the top edge; the cap overhangs both sides' },
49
+ 'shark-fin': { name: 'Shark fin', feature: 'Gray dorsal fin with a lighter leading plane', colors: ['#8b95a5', '#a7b1c0'], mount: 'Fin base spans the top edge center' },
50
+ 'propeller-cap': { name: 'Propeller cap', feature: 'Yellow and red cap dome with a spinning propeller on a button', colors: ['#f6b500', '#e8654f', '#c9d2ff'], mount: 'Dome overlaps the top edge; the propeller spins flat at full size' },
51
+ 'graduation-cap': { name: 'Graduation cap', feature: 'Black mortarboard with a gold button and a swinging tassel', colors: ['#2b2f3a', '#3c4658', '#f6b500'], mount: 'Skull cap rests on the top edge; the board floats above it' },
52
+ 'ice-cream': { name: 'Ice cream', feature: 'Strawberry scoop with an upright cone and a cherry', colors: ['#f2a0b4', '#e0ad84', '#e8654f'], mount: 'Scoop rests on the top edge; the cone points up' },
53
+ 'flower-crown': { name: 'Flower crown', feature: 'Green wreath with alternating pink and yellow blossoms', colors: ['#89b875', '#f2a0b4', '#f6b500'], mount: 'Wreath follows the top edge without a brim' },
54
+ 'pirate-hat': { name: 'Pirate hat', feature: 'Black tricorn with an upturned brim and a white skull mark', colors: ['#2b2f3a', '#3c4658', '#ffffff'], mount: 'Brim rests on the top edge; points stay clear of the arms' },
55
+ lightning: { name: 'Lightning', feature: 'Yellow bolt standing upright with a breathing glow', colors: ['#ffcf11', '#fff3c4'], mount: 'Lower tip touches the top edge center' },
56
+ 'crystal-cluster': { name: 'Crystal cluster', feature: 'Three violet crystals with a lighter facet and a breathing glow', colors: ['#8b5cf6', '#a78bfa', '#c4b5fd'], mount: 'Crystal bases sit on the top edge' },
57
+ ufo: { name: 'UFO', feature: 'Hovering saucer with a glass dome, blinking lights and a tractor beam', colors: ['#8b95a5', '#a2c5d1', '#c7f6ff'], mount: 'Saucer floats above the head; the beam lands on the top edge' },
58
+
59
+ shades: { name: 'Shades', feature: 'Two dark lenses with a short bridge pushed up on the brow', colors: ['#2b2f3a', '#6b7280'], mount: 'Fits within the forehead band above the screen' },
60
+ 'forehead-goggles': { name: 'Forehead goggles', feature: 'Two blue-gray lenses with a center bridge', colors: ['#8b7568', '#a2c5d1', '#dec8ae'], mount: 'Fits within the forehead band; bridge anchors above the screen' },
61
+ 'mecha-faceplate': { name: 'Mecha faceplate', feature: 'White face armor with a dark visor band, angular yellow-green eyes, a red chin plate and two vents', colors: ['#f4f6fb', '#1d2433', '#d94a3a'], mount: 'Replaces the face inside the white screen frame; the eyes change light, not shape' },
62
+ 'pixel-screen': { name: 'Pixel screen', feature: 'Dark monitor with phosphor-green pixel eyes: blocks, a blocky smile, a loading bar and dim dashes', colors: ['#1f2a3a', '#7dff8a'], mount: 'Replaces the face inside the white screen frame; pixel glyphs carry all four expressions' },
63
+ 'gold-faceplate': { name: 'Gold faceplate', feature: 'Armored gold plate whose fixed eye slits change light, not shape: cyan idle, warm flare, amber scan, dim ember', colors: ['#efc04a', '#bf8a2a', '#6fdcff'], mount: 'Replaces the face inside the white screen frame; light carries all four expressions' },
64
+ 'neon-visor': { name: 'Neon visor', feature: 'Full smoked visor with a pair of LED eyes that lid, squint and rest, plus a passing scan line', colors: ['#1d2433', '#4de3ff', '#c7f6ff'], mount: 'Replaces the face inside the white screen frame; the LED pair carries all four expressions' },
65
+
66
+ 'task-board': { name: 'Task board', feature: 'White planning board with connected role-color markers and a yellow accent', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
67
+ wrench: { name: 'Wrench', feature: 'White open-ended wrench with role-color outlines and a yellow handle inset', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
68
+ 'coffee-mug': { name: 'Coffee mug', feature: 'White mug with a role-color band and rising steam', colors: ['#ffffff', '#3161f7', '#5a3c2a'], mount: held },
69
+ shield: { name: 'Shield', feature: 'White shield with a role-color outline and upright check', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
70
+ magnifier: { name: 'Magnifier', feature: 'Upright white lens frame with a role-color outline and a lower grip', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
71
+ 'control-panel': { name: 'Control panel', feature: 'White control panel with dark rails and role-color and yellow knobs', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
72
+ 'magic-wand': { name: 'Magic wand', feature: 'Dark wand tipped with a gold star and drifting sparkles', colors: ['#3b2f5c', '#f6b500', '#fff3c4'], mount: held },
73
+ 'energy-blade': { name: 'Energy blade', feature: 'Compact hilt with a blade that cycles cyan, magenta and green while it extends and retracts', colors: ['#2a3140', '#4de3ff', '#e6fbff'], mount: `${held}; the blade rises beside the body, never across the screen` },
74
+ paintbrush: { name: 'Paintbrush', feature: 'Wooden brush with a steel ferrule and a role-color paint tip', colors: ['#e0ad84', '#8b95a5', '#3161f7'], mount: held },
75
+ 'boba-tea': { name: 'Boba tea', feature: 'White cup of milk tea with dark pearls and a yellow straw', colors: ['#ffffff', '#e0ad84', '#3c4658'], mount: held },
76
+ gamepad: { name: 'Gamepad', feature: 'White controller with a role-color d-pad and two buttons', colors: ['#ffffff', '#3161f7', '#f6b500'], mount: held },
77
+ flag: { name: 'Flag', feature: 'Steel pole with a waving role-color pennant', colors: ['#8b95a5', '#3161f7'], mount: `${held}; the pennant rises beside the body` },
78
+ lantern: { name: 'Lantern', feature: 'Red paper lantern with dark caps and a warm breathing glow', colors: ['#e8654f', '#3c4658', '#ffd970'], mount: held },
79
+ staff: { name: 'Staff', feature: 'Wooden staff topped with a glowing orb that shifts violet, cyan and pink', colors: ['#8a6520', '#8b5cf6', '#c4b5fd'], mount: `${held}; the orb rises beside the body` },
80
+
81
+ cape: { name: 'Cape', feature: 'Crimson cape that flares wide below the body with a lighter lining', colors: ['#b23a48', '#d65a68'], mount: 'Collar sits at the shoulders behind the body; the flares show beside and below the base' },
82
+ 'crescent-moon': { name: 'Crescent moon', feature: 'Yellow crescent rising behind the head with a small star', colors: ['#f6d365', '#fff3c4'], mount: 'Moon shows behind the upper-left of the head' },
83
+ jetpack: { name: 'Jetpack', feature: 'Two steel tanks with red caps and flickering flames', colors: ['#8b95a5', '#e8654f', '#ffb347'], mount: 'Tanks show beside both arms behind the body; flames flicker below' },
84
+ 'star-trail': { name: 'Star trail', feature: 'Three gold four-point stars twinkling beside the body', colors: ['#f6b500', '#fff3c4'], mount: 'Stars float beside the body and twinkle at full size' },
85
+ balloons: { name: 'Balloons', feature: 'Three balloons on strings rising beside the head', colors: ['#e8654f', '#f6b500', '#4f7cf6'], mount: 'Strings meet behind the left shoulder; the bunch bobs at full size' },
86
+ 'bat-wings': { name: 'Bat wings', feature: 'Two scalloped violet wings with darker membranes that flap', colors: ['#5b3a8c', '#3d2563'], mount: 'Wings spread from behind the shoulders and stay clear of the head' },
87
+ 'butterfly-wings': { name: 'Butterfly wings', feature: 'Two pink two-lobed wings with cream inner planes that flap', colors: ['#f2a0b4', '#fff3c4'], mount: 'Wings spread from behind the shoulders' },
88
+ halo: { name: 'Halo', feature: 'Flat golden aureole centered on the antenna light with a breathing glow', colors: ['#f6b500', '#ffe08a', '#fff7dc'], mount: 'Ring sits behind the light and hats; the light disc covers its center' },
89
+ 'angel-wings': { name: 'Angel wings', feature: 'Two white feathered wings with a soft glow and a slow flap', colors: ['#ffffff', '#eef1ff', '#c9d2ff'], mount: 'Wings spread from behind the shoulders' },
90
+ 'sun-rays': { name: 'Sun rays', feature: 'Twelve alternating gold rays turning slowly behind the body', colors: ['#f6b500', '#ffcf11'], mount: 'Rays radiate from the body center' },
91
+ 'orbit-ring': { name: 'Orbit ring', feature: 'Tilted orbit belt with three bodies that circle in front of and behind the robot', colors: ['#b9c4ff', '#f6b500', '#ff9ad9'], mount: 'Belt crosses low around the body; bodies pass in front below the screen' },
92
+ 'dragon-wings': { name: 'Dragon wings', feature: 'Two large crimson wings with dark membranes and ember tips that flap', colors: ['#b23a48', '#7f2634', '#ffb347'], mount: 'Wings spread wide from behind the shoulders' },
93
+
94
+ stripes: { name: 'Stripes', feature: 'Diagonal light stripes across the body and arms', colors: ['#ffffff'], mount: 'One continuous overlay across the body and arms; reads through every palette' },
95
+ chrome: { name: 'Chrome', feature: 'Mirror steel with a horizon line and a sweeping sheen', colors: ['#f4f7fb', '#9aa6b8', '#3c4658'], mount: 'Replaces the body and arm paint; face marks keep the palette' },
96
+ gold: { name: 'Gold', feature: 'Polished gold with a sweeping sheen', colors: ['#f6d365', '#b8862c', '#fff3c4'], mount: 'Replaces the body and arm paint; face marks keep the palette' },
97
+ lava: { name: 'Lava', feature: 'Molten rock whose glow flows between ember red and bright orange', colors: ['#e8451f', '#ffb347', '#4a1208'], mount: 'Replaces the body and arm paint; face marks keep the palette' },
98
+ holographic: { name: 'Holographic', feature: 'Iridescent foil whose colors flow across the body', colors: ['#ff8ad4', '#8ad8ff', '#c9ff8a'], mount: 'Replaces the body and arm paint; face marks keep the palette' },
99
+ galaxy: { name: 'Galaxy', feature: 'Deep space nebula that drifts between violet and blue with twinkling stars', colors: ['#4c1d95', '#2563eb', '#ffffff'], mount: 'Replaces the body and arm paint; stars twinkle on the surface' },
100
+ }
101
+
102
+ export const decorations: Decoration[] = items.map(item => ({ ...item, ...copy[item.id] }))
103
+ export const decorationOf = (id: ItemId): Decoration => ({ ...itemOf(id), ...copy[id] })
104
+ export const decorationName = (id: ItemId | 'none') => id === 'none' ? 'None' : copy[id].name
105
+ export function decorationsOf(slot: Slot): Decoration[] { return decorations.filter(item => item.slot === slot) }
106
+ export function decorationsByRarity(rarity: Rarity): Decoration[] { return decorations.filter(item => item.rarity === rarity) }
107
+ export function decorationsBySeries(series: Series): Decoration[] { return decorations.filter(item => item.series === series) }
108
+ export const rarityOrder = (rarity: Rarity) => rarities.indexOf(rarity)
109
+ export const slotOrder = (slot: Slot) => slots.indexOf(slot)
110
+ export const seriesOrder = (series: Series) => seriesIds.indexOf(series)
@@ -1,4 +1,4 @@
1
- import type { PrimaryId } from './appearanceModel.js'
1
+ import type { HeadId } from './items.js'
2
2
  import type { AvatarPose } from './characters.js'
3
3
 
4
4
  export type MotionKind = 'lift' | 'fitted' | 'bounce' | 'rock' | 'squish' | 'sprout'
@@ -6,10 +6,13 @@ export interface MotionProfile { kind: MotionKind; x: number; y: number; height:
6
6
  const lift = (x = 512, y = 400): MotionProfile => ({ kind: 'lift', x, y, height: .05, angle: 3, shadow: 95 })
7
7
  const fitted = (x = 512): MotionProfile => ({ kind: 'fitted', x, y: 400, height: 0, angle: 3, shadow: 0 })
8
8
  const bounce = (x = 512): MotionProfile => ({ kind: 'bounce', x, y: 400, height: .06, angle: 5, shadow: 64 })
9
- export const decorationMotionProfiles: Record<Exclude<PrimaryId, 'none'>, MotionProfile> = {
9
+ export const decorationMotionProfiles: Record<HeadId, MotionProfile> = {
10
10
  'baseball-cap': lift(478), 'bucket-hat': lift(), beret: lift(494), beanie: fitted(),
11
11
  'top-hat': lift(), 'wizard-hat': lift(), 'chef-hat': lift(), 'party-hat': lift(), crown: lift(),
12
- 'hard-hat': fitted(), nightcap: fitted(493), 'straw-hat': lift(),
12
+ 'hard-hat': fitted(), nightcap: fitted(493), 'straw-hat': lift(), 'ringed-planet': { ...lift(512, 335), shadow: 110 },
13
+ 'cowboy-hat': lift(), 'graduation-cap': lift(), 'pirate-hat': lift(), lightning: { ...lift(), shadow: 40 }, ufo: { ...lift(512, 300), shadow: 120 },
14
+ 'cat-ears': fitted(), 'shark-fin': fitted(), 'propeller-cap': fitted(), 'flower-crown': fitted(), 'crystal-cluster': fitted(),
15
+ mushroom: bounce(), 'ice-cream': bounce(),
13
16
  poop: bounce(), 'rubber-duck': bounce(520), donut: { ...bounce(), shadow: 40 },
14
17
  banana: { kind: 'rock', x: 487, y: 402, height: 0, angle: 5, shadow: 0 },
15
18
  'paper-boat': { kind: 'rock', x: 512, y: 399, height: 0, angle: 5, shadow: 0 },
@@ -0,0 +1,86 @@
1
+ .dca-robot[data-effects='off'] .dca-fx { display: none; }
2
+
3
+ .dca-fx-sheen { transform: translateX(120px); }
4
+ .dca-fx-orbit-a { transform: translate(450px, 0); }
5
+ .dca-fx-orbit-b { transform: translate(-450px, 0); }
6
+ .dca-fx-orbit-c { transform: translate(0, 104px); }
7
+ .dca-fx-orbit-back { opacity: 0; }
8
+ .dca-fx-sparkle > path { transform-box: fill-box; transform-origin: center; }
9
+
10
+ @keyframes dca-fx-pulse { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
11
+ @keyframes dca-fx-flare { 0%, 100% { opacity: .8; } 12% { opacity: 1; } 20% { opacity: .7; } 28% { opacity: 1; } 60% { opacity: .9; } }
12
+ @keyframes dca-fx-breathe { 0%, 100% { opacity: .2; } 50% { opacity: .45; } }
13
+ @keyframes dca-fx-scan-eyes { 0% { transform: translateX(0); } 100% { transform: translateX(300px); } }
14
+ @keyframes dca-fx-flap { 0% { transform: rotate(-12deg); } 100% { transform: rotate(12deg); } }
15
+ @keyframes dca-fx-sway { 0% { transform: rotate(-1.6deg); } 100% { transform: rotate(1.6deg); } }
16
+ @keyframes dca-fx-blade-length { 0%, 100% { transform: scaleY(1); } 38% { transform: scaleY(.58); } 56% { transform: scaleY(1.08); } }
17
+ @keyframes dca-fx-blade-color { 0%, 100% { fill: #4de3ff; } 33% { fill: #ff6bd6; } 66% { fill: #8dff9a; } }
18
+ @keyframes dca-fx-orb-color { 0%, 100% { fill: #8b5cf6; } 33% { fill: #4de3ff; } 66% { fill: #ff6bd6; } }
19
+ @keyframes dca-fx-spin-flat { 0% { transform: scaleX(1); } 50% { transform: scaleX(.08); } 100% { transform: scaleX(1); } }
20
+ @keyframes dca-fx-swing { 0% { transform: rotate(-10deg); } 100% { transform: rotate(10deg); } }
21
+ @keyframes dca-fx-hover { 0% { transform: translateY(0); } 100% { transform: translateY(-14px); } }
22
+ @keyframes dca-fx-wave { 0% { transform: skewY(-6deg); } 100% { transform: skewY(6deg); } }
23
+ @keyframes dca-fx-rotate { to { transform: rotate(360deg); } }
24
+ @keyframes dca-fx-lava {
25
+ 0%, 100% { stop-color: #4a1208; } 20% { stop-color: #e8451f; } 40% { stop-color: #ffb347; } 60% { stop-color: #ff6a2a; } 80% { stop-color: #7a1f0e; }
26
+ }
27
+ @keyframes dca-fx-galaxy {
28
+ 0%, 100% { stop-color: #1b1040; } 20% { stop-color: #4c1d95; } 40% { stop-color: #2563eb; } 60% { stop-color: #0b3d62; } 80% { stop-color: #7c3aed; }
29
+ }
30
+ @keyframes dca-fx-scan { 0% { transform: translateX(-30px); } 100% { transform: translateX(290px); } }
31
+ @keyframes dca-fx-steam { 0% { opacity: 0; transform: translateY(12px); } 35% { opacity: .8; } 100% { opacity: 0; transform: translateY(-28px); } }
32
+ @keyframes dca-fx-sparkle { 0%, 100% { opacity: .15; transform: scale(.55); } 50% { opacity: 1; transform: scale(1.15); } }
33
+ @keyframes dca-fx-flame { 0% { transform: scaleY(.82) scaleX(.94); opacity: .85; } 100% { transform: scaleY(1.12) scaleX(1.04); opacity: 1; } }
34
+ @keyframes dca-fx-node { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
35
+ @keyframes dca-fx-sheen { 0% { transform: translateX(-440px); } 55% { transform: translateX(740px); } 100% { transform: translateX(740px); } }
36
+ @keyframes dca-fx-holo {
37
+ 0% { stop-color: #ff8ad4; } 20% { stop-color: #ffd28a; } 40% { stop-color: #c9ff8a; }
38
+ 60% { stop-color: #8ad8ff; } 80% { stop-color: #c98aff; } 100% { stop-color: #ff8ad4; }
39
+ }
40
+ @keyframes dca-fx-orbit {
41
+ 0% { transform: translate(450px, 0); } 8.33% { transform: translate(390px, 52px); } 16.67% { transform: translate(225px, 90px); }
42
+ 25% { transform: translate(0, 104px); } 33.33% { transform: translate(-225px, 90px); } 41.67% { transform: translate(-390px, 52px); }
43
+ 50% { transform: translate(-450px, 0); } 58.33% { transform: translate(-390px, -52px); } 66.67% { transform: translate(-225px, -90px); }
44
+ 75% { transform: translate(0, -104px); } 83.33% { transform: translate(225px, -90px); } 91.67% { transform: translate(390px, -52px); }
45
+ 100% { transform: translate(450px, 0); }
46
+ }
47
+ /* The lower half of the orbit (0–50%) is in front of the body, the upper half behind it. */
48
+ @keyframes dca-fx-orbit-show-front { 0%, 49.99% { opacity: 1; } 50%, 100% { opacity: 0; } }
49
+ @keyframes dca-fx-orbit-show-back { 0%, 49.99% { opacity: 0; } 50%, 100% { opacity: 1; } }
50
+
51
+ .dca-robot[data-effects='live'] .dca-fx-pulse { animation: dca-fx-pulse 2.8s ease-in-out infinite; }
52
+ .dca-robot[data-effects='live'] .dca-fx-flare { animation: dca-fx-flare 2.2s ease-in-out infinite; }
53
+ .dca-robot[data-effects='live'] .dca-fx-breathe { animation: dca-fx-breathe 3.2s ease-in-out infinite; }
54
+ .dca-robot[data-effects='live'] .dca-fx-scan-eyes { animation: dca-fx-scan-eyes 1.6s cubic-bezier(.45, 0, .55, 1) infinite alternate; }
55
+ .dca-robot[data-effects='live'] .dca-fx-flap { animation: dca-fx-flap 1.3s ease-in-out infinite alternate; }
56
+ .dca-robot[data-effects='live'] .dca-fx-sway { animation: dca-fx-sway 3.4s ease-in-out infinite alternate; }
57
+ .dca-robot[data-effects='live'] .dca-fx-blade { animation: dca-fx-blade-length 4.4s ease-in-out infinite; }
58
+ .dca-robot[data-effects='live'] .dca-fx-blade-color { animation: dca-fx-blade-color 6s linear infinite; }
59
+ .dca-robot[data-effects='live'] .dca-fx-orb-color { animation: dca-fx-orb-color 5s linear infinite; }
60
+ .dca-robot[data-effects='live'] .dca-fx-spin-flat { animation: dca-fx-spin-flat .5s linear infinite; }
61
+ .dca-robot[data-effects='live'] .dca-fx-swing { animation: dca-fx-swing 1.4s ease-in-out infinite alternate; }
62
+ .dca-robot[data-effects='live'] .dca-fx-hover { animation: dca-fx-hover 2.2s ease-in-out infinite alternate; }
63
+ .dca-robot[data-effects='live'] .dca-fx-wave { animation: dca-fx-wave 1.1s ease-in-out infinite alternate; }
64
+ .dca-robot[data-effects='live'] .dca-fx-rotate { animation: dca-fx-rotate 28s linear infinite; }
65
+ .dca-robot[data-effects='live'] .dca-fx-flap-slow { animation: dca-fx-flap 2.6s ease-in-out infinite alternate; }
66
+ .dca-robot[data-effects='live'] .dca-skin-lava stop { animation: dca-fx-lava 7s ease-in-out infinite; }
67
+ .dca-robot[data-effects='live'] .dca-skin-galaxy stop { animation: dca-fx-galaxy 9s ease-in-out infinite; }
68
+ .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy) stop:nth-child(2) { animation-delay: -1.4s; }
69
+ .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy) stop:nth-child(3) { animation-delay: -2.8s; }
70
+ .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy) stop:nth-child(4) { animation-delay: -4.2s; }
71
+ .dca-robot[data-effects='live'] :is(.dca-skin-lava, .dca-skin-galaxy) stop:nth-child(5) { animation-delay: -5.6s; }
72
+ .dca-robot[data-effects='live'] .dca-fx-scan { animation: dca-fx-scan 2.4s cubic-bezier(.45, 0, .55, 1) infinite alternate; }
73
+ .dca-robot[data-effects='live'] .dca-fx-steam { animation: dca-fx-steam 2.6s ease-out infinite; }
74
+ .dca-robot[data-effects='live'] .dca-fx-sparkle > path { animation: dca-fx-sparkle 1.8s ease-in-out infinite; }
75
+ .dca-fx-sparkle-solo { transform-box: fill-box; transform-origin: center; }
76
+ .dca-robot[data-effects='live'] .dca-fx-sparkle-solo { animation: dca-fx-sparkle 2.4s ease-in-out infinite; }
77
+ .dca-robot[data-effects='live'] .dca-fx-flame { animation: dca-fx-flame .42s ease-in-out infinite alternate; }
78
+ .dca-robot[data-effects='live'] .dca-fx-node { animation: dca-fx-node 1.8s ease-in-out infinite; }
79
+ .dca-robot[data-effects='live'] .dca-fx-sheen { animation: dca-fx-sheen 4.8s cubic-bezier(.5, .05, .5, .95) infinite; }
80
+ .dca-robot[data-effects='live'] .dca-fx-orbit-front { animation: dca-fx-orbit 8s linear infinite, dca-fx-orbit-show-front 8s linear infinite; }
81
+ .dca-robot[data-effects='live'] .dca-fx-orbit-back { animation: dca-fx-orbit 8s linear infinite, dca-fx-orbit-show-back 8s linear infinite; }
82
+ .dca-robot[data-effects='live'] .dca-skin-holo stop { animation: dca-fx-holo 6s linear infinite; }
83
+ .dca-robot[data-effects='live'] .dca-skin-holo stop:nth-child(2) { animation-delay: -1.2s; }
84
+ .dca-robot[data-effects='live'] .dca-skin-holo stop:nth-child(3) { animation-delay: -2.4s; }
85
+ .dca-robot[data-effects='live'] .dca-skin-holo stop:nth-child(4) { animation-delay: -3.6s; }
86
+ .dca-robot[data-effects='live'] .dca-skin-holo stop:nth-child(5) { animation-delay: -4.8s; }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
+ export * from './items.js'
1
2
  export * from './appearanceModel.js'
2
3
  export * from './characters.js'
3
4
  export * from './palette.js'
5
+ export * from './paintMaterials.js'
4
6
  export * from './decorationCatalog.js'
5
7
  export * from './decorationMotion.js'
package/src/items.ts ADDED
@@ -0,0 +1,86 @@
1
+ export const slots = ['head', 'face', 'hand', 'back', 'skin'] as const
2
+ export type Slot = typeof slots[number]
3
+ export const rarities = ['common', 'uncommon', 'rare', 'epic', 'legendary'] as const
4
+ export type Rarity = typeof rarities[number]
5
+ export const seriesIds = ['everyday', 'dev', 'tech', 'fantasy', 'nature', 'snack', 'critters'] as const
6
+ export type Series = typeof seriesIds[number]
7
+ export const zones = ['top', 'brow', 'rim', 'screen', 'hand', 'back', 'body'] as const
8
+ export type Zone = typeof zones[number]
9
+
10
+ export interface ItemSpec<S extends Slot = Slot, I extends string = string> {
11
+ id: I
12
+ slot: S
13
+ rarity: Rarity
14
+ series: Series
15
+ zones: readonly Zone[]
16
+ }
17
+
18
+ const hat = <I extends string>(id: I, rarity: Rarity, series: Series = 'everyday') =>
19
+ ({ id, slot: 'head', rarity, series, zones: ['top', 'brow'] }) as const satisfies ItemSpec<'head', I>
20
+ const object = <I extends string>(id: I, rarity: Rarity, series: Series) =>
21
+ ({ id, slot: 'head', rarity, series, zones: ['top'] }) as const satisfies ItemSpec<'head', I>
22
+ const brow = <I extends string>(id: I, rarity: Rarity, series: Series) =>
23
+ ({ id, slot: 'face', rarity, series, zones: ['brow'] }) as const satisfies ItemSpec<'face', I>
24
+ const faceplate = <I extends string>(id: I, rarity: Rarity, series: Series) =>
25
+ ({ id, slot: 'face', rarity, series, zones: ['screen'] }) as const satisfies ItemSpec<'face', I>
26
+ const hand = <I extends string>(id: I, rarity: Rarity, series: Series) =>
27
+ ({ id, slot: 'hand', rarity, series, zones: ['hand'] }) as const satisfies ItemSpec<'hand', I>
28
+ const back = <I extends string>(id: I, rarity: Rarity, series: Series) =>
29
+ ({ id, slot: 'back', rarity, series, zones: ['back'] }) as const satisfies ItemSpec<'back', I>
30
+ const skin = <I extends string>(id: I, rarity: Rarity, series: Series) =>
31
+ ({ id, slot: 'skin', rarity, series, zones: ['body'] }) as const satisfies ItemSpec<'skin', I>
32
+
33
+ // Registry order is presentation order only; sampling is weighted by rarity, then uniform.
34
+ export const items = [
35
+ hat('baseball-cap', 'common'), hat('bucket-hat', 'common'), hat('beret', 'uncommon'), hat('beanie', 'common'),
36
+ hat('top-hat', 'rare'), hat('wizard-hat', 'rare', 'fantasy'), hat('chef-hat', 'uncommon', 'snack'), hat('party-hat', 'uncommon'),
37
+ hat('crown', 'epic', 'fantasy'), hat('hard-hat', 'common'), hat('nightcap', 'uncommon'), hat('straw-hat', 'common'),
38
+ object('poop', 'uncommon', 'everyday'), object('banana', 'common', 'snack'), object('fried-egg', 'common', 'snack'),
39
+ object('rubber-duck', 'uncommon', 'critters'), object('paper-boat', 'common', 'everyday'), object('traffic-cone', 'common', 'everyday'),
40
+ object('sprout', 'common', 'nature'), object('donut', 'uncommon', 'snack'), object('ringed-planet', 'legendary', 'tech'),
41
+ object('cat-ears', 'common', 'critters'), hat('cowboy-hat', 'common'), object('mushroom', 'common', 'nature'),
42
+ object('shark-fin', 'uncommon', 'critters'), hat('propeller-cap', 'uncommon'), hat('graduation-cap', 'uncommon', 'dev'),
43
+ object('ice-cream', 'uncommon', 'snack'), object('flower-crown', 'rare', 'nature'), hat('pirate-hat', 'rare', 'fantasy'),
44
+ object('lightning', 'epic', 'tech'), object('crystal-cluster', 'epic', 'fantasy'), object('ufo', 'legendary', 'tech'),
45
+
46
+ brow('shades', 'common', 'everyday'), brow('forehead-goggles', 'uncommon', 'tech'),
47
+ faceplate('mecha-faceplate', 'rare', 'tech'), faceplate('pixel-screen', 'rare', 'dev'),
48
+ faceplate('gold-faceplate', 'epic', 'tech'), faceplate('neon-visor', 'legendary', 'tech'),
49
+
50
+ hand('task-board', 'common', 'dev'), hand('wrench', 'common', 'dev'), hand('coffee-mug', 'common', 'dev'),
51
+ hand('paintbrush', 'common', 'everyday'), hand('boba-tea', 'common', 'snack'),
52
+ hand('shield', 'uncommon', 'dev'), hand('magnifier', 'uncommon', 'dev'), hand('gamepad', 'uncommon', 'dev'), hand('flag', 'uncommon', 'everyday'),
53
+ hand('control-panel', 'rare', 'tech'), hand('lantern', 'rare', 'fantasy'),
54
+ hand('magic-wand', 'epic', 'fantasy'), hand('staff', 'epic', 'fantasy'), hand('energy-blade', 'legendary', 'tech'),
55
+
56
+ back('cape', 'common', 'fantasy'), back('crescent-moon', 'common', 'nature'),
57
+ back('jetpack', 'uncommon', 'tech'), back('star-trail', 'uncommon', 'fantasy'), back('balloons', 'uncommon', 'everyday'),
58
+ back('bat-wings', 'rare', 'critters'), back('butterfly-wings', 'rare', 'critters'),
59
+ back('halo', 'epic', 'fantasy'), back('angel-wings', 'epic', 'fantasy'), back('sun-rays', 'epic', 'nature'),
60
+ back('orbit-ring', 'legendary', 'tech'), back('dragon-wings', 'legendary', 'fantasy'),
61
+
62
+ skin('stripes', 'common', 'everyday'),
63
+ skin('chrome', 'epic', 'tech'), skin('gold', 'epic', 'fantasy'), skin('lava', 'epic', 'nature'),
64
+ skin('holographic', 'legendary', 'tech'), skin('galaxy', 'legendary', 'fantasy'),
65
+ ] as const
66
+
67
+ export type Item = typeof items[number]
68
+ export type ItemId = Item['id']
69
+ export type ItemOf<S extends Slot> = Extract<Item, { slot: S }>['id']
70
+ export type HeadId = ItemOf<'head'>
71
+ export type FaceId = ItemOf<'face'>
72
+ export type HandId = ItemOf<'hand'>
73
+ export type BackId = ItemOf<'back'>
74
+ export type SkinId = ItemOf<'skin'>
75
+
76
+ const byId = new Map<string, Item>(items.map(item => [item.id, item]))
77
+ export function itemOf(id: ItemId): Item { return byId.get(id)! }
78
+ export function isItemId(id: string): id is ItemId { return byId.has(id) }
79
+ export function itemsOf<S extends Slot>(slot: S): Extract<Item, { slot: S }>[] {
80
+ return items.filter((item): item is Extract<Item, { slot: S }> => item.slot === slot)
81
+ }
82
+ export function slotIds<S extends Slot>(slot: S): ItemOf<S>[] { return itemsOf(slot).map(item => item.id) as ItemOf<S>[] }
83
+ export function conflicts(a: ItemId, b: ItemId): boolean {
84
+ const right: readonly Zone[] = itemOf(b).zones
85
+ return itemOf(a).zones.some(zone => right.includes(zone))
86
+ }
@@ -0,0 +1,18 @@
1
+ import type { SkinId } from './items.js'
2
+
3
+ export interface PaintMaterial {
4
+ shadow: string
5
+ accent: string
6
+ }
7
+
8
+ export const paintMaterials = {
9
+ chrome: { shadow: '#1f2733', accent: '#c3ccd8' },
10
+ holographic: { shadow: '#5b3a8c', accent: '#c98aff' },
11
+ gold: { shadow: '#5a3a10', accent: '#f6d365' },
12
+ lava: { shadow: '#3a0a05', accent: '#ff6a2a' },
13
+ galaxy: { shadow: '#0b0620', accent: '#7c3aed' },
14
+ } as const satisfies Partial<Record<SkinId, PaintMaterial>>
15
+
16
+ export type PaintSkinId = keyof typeof paintMaterials
17
+ export function isPaintSkin(id: SkinId | 'none'): id is PaintSkinId { return id in paintMaterials }
18
+ export function paintMaterialOf(id: SkinId | 'none'): PaintMaterial | undefined { return isPaintSkin(id) ? paintMaterials[id] : undefined }
package/src/palette.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { SkinId } from './items.js'
2
+ import { paintMaterialOf } from './paintMaterials.js'
3
+
1
4
  export interface PaletteEntry {
2
5
  key: string
3
6
  bodyD: string
@@ -46,7 +49,8 @@ export const PALETTE: PaletteEntry[] = [
46
49
 
47
50
 
48
51
  export function paletteOf(spec: { palette: number }): PaletteEntry { return PALETTE[spec.palette] ?? DEFAULT_MASCOT_PALETTE }
49
- export function mascotPaletteOf(spec?: { palette: number }): MascotPaintPalette {
50
- if (!spec || spec.palette === -1) return DEFAULT_MASCOT_PALETTE
51
- const paint = paletteOf(spec); return { ...paint, markM: paint.markL }
52
+ export function mascotPaletteOf(spec?: { palette: number; skin?: SkinId | 'none' }): MascotPaintPalette {
53
+ const material = paintMaterialOf(spec?.skin ?? 'none')
54
+ const paint: MascotPaintPalette = !spec || spec.palette === -1 ? DEFAULT_MASCOT_PALETTE : { ...paletteOf(spec), markM: paletteOf(spec).markL }
55
+ return material ? { ...paint, ...material } : paint
52
56
  }