feat: added ability to handle sp variant

This commit is contained in:
Haoyu Xu
2025-08-03 13:09:24 +08:00
parent 2690eee3b9
commit 53e6ca4c71
11 changed files with 107 additions and 46 deletions

View File

@@ -77,6 +77,7 @@ export default function Operator() {
const [voiceSrc, setVoiceSrc] = useState(null) const [voiceSrc, setVoiceSrc] = useState(null)
const [isVoicePlaying, _setIsVoicePlaying] = useState(false) const [isVoicePlaying, _setIsVoicePlaying] = useState(false)
const isVoicePlayingRef = useRef(isVoicePlaying) const isVoicePlayingRef = useRef(isVoicePlaying)
const [spineAnimationList, setSpineAnimationList] = useState([])
const setVoiceLang = (value) => { const setVoiceLang = (value) => {
voiceLangRef.current = value voiceLangRef.current = value
@@ -154,7 +155,7 @@ export default function Operator() {
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setTitle(getPartialName('name', config.codename[language])) setTitle(config.codename[language])
} }
}, [config, language, key, setTitle]) }, [config, language, key, setTitle])
@@ -180,6 +181,9 @@ export default function Operator() {
fps: 60, fps: 60,
defaultMix: 0.3, defaultMix: 0.3,
success: (player) => { success: (player) => {
setSpineAnimationList(
player.skeleton.data.animations.map((e) => e.name)
)
if ( if (
player.skeleton.data.animations player.skeleton.data.animations
.map((e) => e.name) .map((e) => e.name)
@@ -283,9 +287,9 @@ export default function Operator() {
(animation) => { (animation) => {
if (voiceLangRef.current) { if (voiceLangRef.current) {
let id = null let id = null
if (animation === 'Idle') id = 'CN_011' if (animation.startsWith('Idle')) id = 'CN_011'
if (animation === 'Interact') id = 'CN_034' if (animation.startsWith('Interact')) id = 'CN_034'
if (animation === 'Special') id = 'CN_042' if (animation.startsWith('Special')) id = 'CN_042'
if (id) { if (id) {
setCurrentVoiceId(id) setCurrentVoiceId(id)
setVoiceSrc( setVoiceSrc(
@@ -320,29 +324,15 @@ export default function Operator() {
const spineSettings = [ const spineSettings = [
{ {
name: 'animation', name: 'animation',
options: [ options: spineAnimationList.map((name) => {
{ return {
name: 'idle', name,
onClick: () => setSpineAnimation('Idle'), onClick: () => setSpineAnimation(name),
activeRule: () => { activeRule: () => {
return spineAnimationName === 'Idle' return spineAnimationName === name
}, },
}, }
{ }),
name: 'interact',
onClick: () => setSpineAnimation('Interact'),
activeRule: () => {
return spineAnimationName === 'Interact'
},
},
{
name: 'special',
onClick: () => setSpineAnimation('Special'),
activeRule: () => {
return spineAnimationName === 'Special'
},
},
],
}, },
{ {
name: 'voice', name: 'voice',

View File

@@ -16,6 +16,7 @@ export default class Player {
#resetTime = window.performance.now() #resetTime = window.performance.now()
#isPlayingInteract = false #isPlayingInteract = false
#spine #spine
#animationList = []
#default = { #default = {
fps: 60, fps: 60,
padding: { padding: {
@@ -62,6 +63,9 @@ export default class Player {
fps: 60, fps: 60,
defaultMix: 0, defaultMix: 0,
success: function (widget) { success: function (widget) {
_this.#animationList = widget.skeleton.data.animations.map(
(e) => e.name
)
if ( if (
widget.skeleton.data.animations widget.skeleton.data.animations
.map((e) => e.name) .map((e) => e.name)
@@ -70,10 +74,15 @@ export default class Player {
) { ) {
widget.animationState.setAnimation(0, 'Start', false) widget.animationState.setAnimation(0, 'Start', false)
} }
widget.animationState.addAnimation(0, 'Idle', true, 0) widget.animationState.addAnimation(
0,
_this.#selectRandomAnimation('Idle'),
true,
0
)
widget.animationState.addListener({ widget.animationState.addListener({
end: (e) => { end: (e) => {
if (e.animation.name == 'Interact') { if (e.animation.name.startsWith('Interact')) {
_this.#isPlayingInteract = false _this.#isPlayingInteract = false
} }
}, },
@@ -85,13 +94,13 @@ export default class Player {
_this.#resetTime = performance.now() _this.#resetTime = performance.now()
let entry = widget.animationState.setAnimation( let entry = widget.animationState.setAnimation(
0, 0,
'Special', _this.#selectRandomAnimation('Special'),
false false
) )
entry.mixDuration = 0.3 entry.mixDuration = 0.3
widget.animationState.addAnimation( widget.animationState.addAnimation(
0, 0,
'Idle', _this.#selectRandomAnimation('Idle'),
true, true,
0 0
) )
@@ -105,11 +114,16 @@ export default class Player {
_this.#isPlayingInteract = true _this.#isPlayingInteract = true
let entry = widget.animationState.setAnimation( let entry = widget.animationState.setAnimation(
0, 0,
'Interact', _this.#selectRandomAnimation('Interact'),
false false
) )
entry.mixDuration = 0.3 entry.mixDuration = 0.3
widget.animationState.addAnimation(0, 'Idle', true, 0) widget.animationState.addAnimation(
0,
_this.#selectRandomAnimation('Idle'),
true,
0
)
} }
document.dispatchEvent(Events.Ready.handler()) document.dispatchEvent(Events.Ready.handler())
}, },
@@ -124,10 +138,7 @@ export default class Player {
success() { success() {
this.#loadViewport() this.#loadViewport()
updateHTMLOptions( updateHTMLOptions(this.#animationList, 'animation-selection')
this.#spine.skeleton.data.animations.map((e) => e.name),
'animation-selection'
)
} }
resetPadding() { resetPadding() {
@@ -176,6 +187,13 @@ export default class Player {
}) })
} }
#selectRandomAnimation(name) {
const animationList = this.#animationList.filter((animation) =>
animation.startsWith(name)
)
return animationList[Math.floor(Math.random() * animationList.length)]
}
get usePadding() { get usePadding() {
return this.#config.usePadding return this.#config.usePadding
} }

View File

@@ -39,6 +39,10 @@ module:
title: title:
zh-CN: '明日方舟:' zh-CN: '明日方舟:'
en-US: 'Arknights: ' en-US: 'Arknights: '
sp_filename_prefix: sp_
sp_title:
zh-CN: '「SP」 '
en-US: '[SP] '
project_json: project_json:
project_json: project.json project_json: project.json
preview_jpg: preview.jpg preview_jpg: preview.jpg

View File

@@ -1,3 +1,8 @@
export type TitleLanguages = {
'zh-CN': string
'en-US': string
}
export type Config = { export type Config = {
site_id: string site_id: string
total_size: number total_size: number
@@ -44,10 +49,9 @@ export type Config = {
Texture2D: string Texture2D: string
character_table_json: string character_table_json: string
skin_table_json: string skin_table_json: string
title: { title: TitleLanguages
'zh-CN': string sp_filename_prefix: string
'en-US': string sp_title: TitleLanguages
}
} }
project_json: { project_json: {
project_json: string project_json: string

View File

@@ -64,6 +64,7 @@ ines_melodic_flutter: !include config/ines_melodic_flutter.yaml
wisadel_supernova: !include config/wisadel_supernova.yaml wisadel_supernova: !include config/wisadel_supernova.yaml
archetto_glory_of_the_devout: !include config/archetto_glory_of_the_devout.yaml archetto_glory_of_the_devout: !include config/archetto_glory_of_the_devout.yaml
kroos_moonlit_voyage: !include config/kroos_moonlit_voyage.yaml kroos_moonlit_voyage: !include config/kroos_moonlit_voyage.yaml
kroos_moonlit_voyage_sp: !include config/kroos_moonlit_voyage_sp.yaml
exusiai_the_new_covenant: !include config/exusiai_the_new_covenant.yaml exusiai_the_new_covenant: !include config/exusiai_the_new_covenant.yaml
ascalon_phototaxis: !include config/ascalon_phototaxis.yaml ascalon_phototaxis: !include config/ascalon_phototaxis.yaml
lin_summer_flowers_fa137: !include config/lin_summer_flowers_fa137.yaml lin_summer_flowers_fa137: !include config/lin_summer_flowers_fa137.yaml

View File

@@ -0,0 +1,6 @@
codename:
zh-CN: 星月漂流记 · 克洛丝
en-US: Moonlit Voyage / Kroos
official_id: '202504992'
isSP: true
viewport_top: 2

View File

@@ -1,3 +1,4 @@
import { TitleLanguages } from './../config/types'
import path from 'node:path' import path from 'node:path'
import { yaml, file, alphaComposite } from '@aklive2d/libs' import { yaml, file, alphaComposite } from '@aklive2d/libs'
import config from '@aklive2d/config' import config from '@aklive2d/config'
@@ -53,8 +54,10 @@ export const generateAssetsJson = async (
extractedDir: string, extractedDir: string,
targetDir: string, targetDir: string,
_opts: { _opts: {
isSP?: boolean
useSymLink?: boolean useSymLink?: boolean
} = { } = {
isSP: false,
useSymLink: true, useSymLink: true,
} }
) => { ) => {
@@ -64,7 +67,7 @@ export const generateAssetsJson = async (
* Special Cases: * Special Cases:
* - ines_melodic_flutter * - ines_melodic_flutter
*/ */
filename = getActualFilename(filename, extractedDir) filename = getActualFilename(filename, extractedDir, _opts.isSP)
const skelFilename = findSkel(filename, extractedDir) const skelFilename = findSkel(filename, extractedDir)
const atlasFilename = `${filename}.atlas` const atlasFilename = `${filename}.atlas`
@@ -160,8 +163,23 @@ const generateMapping = () => {
type === 'skin' type === 'skin'
? skinEntry.skinId.replace(/@/, '_') ? skinEntry.skinId.replace(/@/, '_')
: `${skinEntry.charId}_2` : `${skinEntry.charId}_2`
const regions = Object.keys(
operator.codename
) as (keyof TitleLanguages)[]
if (operator.isSP) {
regions.forEach((region: keyof TitleLanguages) => {
operator.codename[region] =
`${config.module.operator.sp_title[region]}${operator.codename[region]}`
})
}
// add title // add title
operator.title = `${config.module.operator.title['en-US']}${operator.codename['en-US']} - ${config.module.operator.title['zh-CN']}${operator.codename['zh-CN']}` operator.title = regions
.map(
(region: keyof TitleLanguages) =>
`${config.module.operator.title[region]}${operator.codename[region]}`
)
.join(' - ')
// add type // add type
operator.type = operatorInfo.type operator.type = operatorInfo.type

View File

@@ -112,6 +112,9 @@ const generateAssets = async (name: string) => {
await generateAssetsJson( await generateAssetsJson(
operators[name].filename, operators[name].filename,
extractedDir, extractedDir,
getDistFolder(name) getDistFolder(name),
{
isSP: operators[name].isSP,
}
) )
} }

View File

@@ -168,7 +168,13 @@ export const findCodename = (
return codename return codename
} }
export const getActualFilename = (filename: string, dir: string) => { export const getActualFilename = (
filename: string,
dir: string,
isSP: boolean = false
) => {
if (isSP)
filename = `${config.module.operator.sp_filename_prefix}${filename}`
const files = file.readdirSync(dir) const files = file.readdirSync(dir)
const actualFilename = files.find((e) => { const actualFilename = files.find((e) => {
const name = path.parse(e).name const name = path.parse(e).name

View File

@@ -21,6 +21,7 @@ export interface OperatorConfig {
date: string date: string
voice_id: string | null voice_id: string | null
color: string color: string
isSP: boolean // kroos_moonlit_voyage_sp
} }
export type Config = { export type Config = {

View File

@@ -21,6 +21,8 @@ interface DirectoryOperatorConfig extends OperatorConfig {
workshopId: string | null workshopId: string | null
} }
const SPINE_FILENAME_PREFIX = 'dyn_illust_'
export const copyShowcaseData = ( export const copyShowcaseData = (
name: string, name: string,
{ {
@@ -37,7 +39,13 @@ export const copyShowcaseData = (
) )
const spineFilenames = file const spineFilenames = file
.readdirSync(operatorAssetsDir) .readdirSync(operatorAssetsDir)
.filter((item) => item.startsWith('dyn_illust_')) .filter((item) =>
item.startsWith(
operators[name].isSP
? `${config.module.operator.sp_filename_prefix}${SPINE_FILENAME_PREFIX}`
: SPINE_FILENAME_PREFIX
)
)
const q = [ const q = [
{ {
fn: file.symlink, fn: file.symlink,
@@ -113,7 +121,8 @@ export const copyShowcaseData = (
}) })
const filename = getActualFilename( const filename = getActualFilename(
operators[name].filename, operators[name].filename,
getExtractedFolder(name) getExtractedFolder(name),
operators[name].isSP
) )
const buildConfig = { const buildConfig = {
insight_id: config.insight.id, insight_id: config.insight.id,
@@ -201,7 +210,8 @@ export const copyDirectoryData = async ({
const curD = cur as DirectoryOperatorConfig const curD = cur as DirectoryOperatorConfig
curD.filename = getActualFilename( curD.filename = getActualFilename(
operators[curD.link].filename, operators[curD.link].filename,
getExtractedFolder(curD.link) getExtractedFolder(curD.link),
operators[curD.link].isSP
) )
curD.use_json = findSkel( curD.use_json = findSkel(
curD.filename, curD.filename,