feat: refine Memento headset expressions

This commit is contained in:
Наер Михаил Александрович 2026-08-27 01:26:48 +03:00
parent 1515a7c6cf
commit 271a0dfa2f
2 changed files with 129 additions and 7 deletions

View File

@ -318,11 +318,37 @@ const fixedSkinColorBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehavio
return { expressions: [...expressions, ...scaredExpressions], sequences }
}
const mementoBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrary => {
const mementoHeadsetWorkingExpressionId = 'memento-headset-working-joyful-eyes'
const mementoHeadsetShyExpressionId = 'memento-headset-shy-left'
const copyExpressionEyes = (target: Expression, source: Expression): Expression => ({
...target,
widthLeft: source.widthLeft,
widthRight: source.widthRight,
heightLeft: source.heightLeft,
heightRight: source.heightRight,
curvatureLeft: source.curvatureLeft,
curvatureRight: source.curvatureRight,
spacing: source.spacing,
positionXLeft: source.positionXLeft,
positionXRight: source.positionXRight,
positionYLeft: source.positionYLeft,
positionYRight: source.positionYRight,
leftAngle: source.leftAngle,
rightAngle: source.rightAngle,
eyeMotion: source.eyeMotion,
})
const mementoBehavior = (
behavior?: AvatarBehaviorLibrary,
customizeHeadsetExpressions = false
): AvatarBehaviorLibrary => {
const sourceExpressions = (behavior?.expressions ?? initialExpressions).filter(
expression =>
expression.semanticKey !== 'onboarding-listening' &&
expression.semanticKey !== 'onboarding-curious'
expression.semanticKey !== 'onboarding-curious' &&
expression.semanticKey !== mementoHeadsetWorkingExpressionId &&
expression.semanticKey !== mementoHeadsetShyExpressionId
)
const expressions = sourceExpressions.map(expression => {
const fixedColorExpression = { ...expression }
@ -366,10 +392,53 @@ const mementoBehavior = (behavior?: AvatarBehaviorLibrary): AvatarBehaviorLibrar
steps: sequence.steps.map(step => ({ ...step })),
blink: { ...sequence.blink },
}))
const workingPose = expressions.find(expression => expression.semanticKey === 'angry-right')
const joyfulEyes = expressions.find(expression => expression.semanticKey === 'joyful-wide')
const headsetWorkingExpression =
customizeHeadsetExpressions && workingPose && joyfulEyes
? {
...copyExpressionEyes(workingPose, joyfulEyes),
id: mementoHeadsetWorkingExpressionId,
semanticKey: mementoHeadsetWorkingExpressionId,
}
: undefined
const shySource = expressions.find(expression => expression.semanticKey === 'upward-side-glance')
const headsetShyExpression =
customizeHeadsetExpressions && shySource
? {
...shySource,
id: mementoHeadsetShyExpressionId,
semanticKey: mementoHeadsetShyExpressionId,
headY: -Math.abs(shySource.headY),
spacing: shySource.spacing - 10,
positionYLeft: shySource.positionYLeft + 10,
positionYRight: shySource.positionYRight + 10,
}
: undefined
const sequenceFirstExpression = new Map<string, string>([
...(headsetWorkingExpression ? [['working', headsetWorkingExpression.id] as const] : []),
...(headsetShyExpression ? [['shy', headsetShyExpression.id] as const] : []),
])
const resolvedSequences = sequences.map(sequence => {
const expressionId = sequenceFirstExpression.get(sequence.semanticKey ?? '')
return expressionId
? {
...sequence,
steps: sequence.steps.map((step, index) =>
index === 0 ? { ...step, expressionId } : step
),
}
: sequence
})
return {
expressions: [...expressions, ...onboardingExpressions],
expressions: [
...expressions,
...(headsetWorkingExpression ? [headsetWorkingExpression] : []),
...(headsetShyExpression ? [headsetShyExpression] : []),
...onboardingExpressions,
],
sequences: [
...sequences.filter(sequence => sequence.semanticKey !== 'onboarding'),
...resolvedSequences.filter(sequence => sequence.semanticKey !== 'onboarding'),
...(onboardingExpressions.length === 2
? [
{
@ -884,7 +953,7 @@ const primitiveBundledAvatars: StudioAvatar[] = [
? { ...filledAvatarRenderStyle }
: { ...defaultAvatarRenderStyle },
...(surface === 'ghost'
? { behavior: mementoBehavior() }
? { behavior: mementoBehavior(undefined, id === 'primitive-ghost-headset') }
: surface === 'apple-bite'
? { behavior: fixedSkinColorBehavior() }
: surface === 'lock'
@ -1023,7 +1092,10 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib
avatar.id
)
) {
return { ...avatar, behavior: mementoBehavior(avatar.behavior) }
return {
...avatar,
behavior: mementoBehavior(avatar.behavior, avatar.id === 'primitive-ghost-headset'),
}
}
if (filledBundledAvatarIds.has(avatar.id)) {
return { ...avatar, behavior: fixedSkinColorBehavior(avatar.behavior) }
@ -1177,7 +1249,10 @@ export const ensurePrimitiveBundledAvatars = (library: AvatarLibrary): AvatarLib
].includes(avatarWithAppleBiteEyes.id)
? {
...avatarWithAppleBiteEyes,
behavior: mementoBehavior(avatarWithAppleBiteEyes.behavior),
behavior: mementoBehavior(
avatarWithAppleBiteEyes.behavior,
avatarWithAppleBiteEyes.id === 'primitive-ghost-headset'
),
}
: filledBundledAvatarIds.has(avatarWithAppleBiteEyes.id)
? {

View File

@ -221,6 +221,53 @@ describe('Studio document', () => {
expect(memento?.colors).toEqual({ body: '#ff9d45', eyes: '#111316' })
expect(headsetMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#222222' })
expect(strokeMemento?.colors).toEqual({ body: '#ff9d45', eyes: '#ff9d45' })
const headsetWorkingFirst = headsetMemento?.behavior?.sequences.find(
sequence => sequence.semanticKey === 'working'
)?.steps[0]
const headsetJoyfulFirst = headsetMemento?.behavior?.sequences.find(
sequence => sequence.semanticKey === 'joyful'
)?.steps[0]
const headsetShyFirst = headsetMemento?.behavior?.sequences.find(
sequence => sequence.semanticKey === 'shy'
)?.steps[0]
const headsetWorkingExpression = headsetMemento?.behavior?.expressions.find(
expression => expression.id === headsetWorkingFirst?.expressionId
)
const headsetJoyfulExpression = headsetMemento?.behavior?.expressions.find(
expression => expression.id === headsetJoyfulFirst?.expressionId
)
const headsetShyExpression = headsetMemento?.behavior?.expressions.find(
expression => expression.id === headsetShyFirst?.expressionId
)
expect(headsetWorkingExpression).toMatchObject({
widthLeft: headsetJoyfulExpression?.widthLeft,
widthRight: headsetJoyfulExpression?.widthRight,
heightLeft: headsetJoyfulExpression?.heightLeft,
heightRight: headsetJoyfulExpression?.heightRight,
spacing: headsetJoyfulExpression?.spacing,
leftAngle: headsetJoyfulExpression?.leftAngle,
rightAngle: headsetJoyfulExpression?.rightAngle,
})
expect(headsetWorkingExpression).toMatchObject({
headX: initialExpressions[7].headX,
headY: initialExpressions[7].headY,
headZ: initialExpressions[7].headZ,
})
expect(headsetJoyfulFirst?.expressionId).toBe('expression-11')
expect(headsetShyExpression).toMatchObject({
headY: -Math.abs(initialExpressions[0].headY),
spacing: initialExpressions[0].spacing - 10,
positionYLeft: initialExpressions[0].positionYLeft + 10,
positionYRight: initialExpressions[0].positionYRight + 10,
})
expect(
memento?.behavior?.sequences.find(sequence => sequence.semanticKey === 'joyful')?.steps[0]
.expressionId
).toBe('expression-11')
expect(
strokeMemento?.behavior?.sequences.find(sequence => sequence.semanticKey === 'joyful')
?.steps[0].expressionId
).toBe('expression-11')
expect(
strokeMemento?.behavior?.sequences.some(sequence => sequence.semanticKey === 'onboarding')
).toBe(true)