dsh-pet-live2d-viola 1.0.0
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.
- package/LICENSE +44 -0
- package/README.md +98 -0
- package/assets/characters/miyako/Expressions/exp_amazed01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_angry01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_angry02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_angry03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_angry04.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_angry05.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_anxiety01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_bsmile01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_bsmile02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_cry01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_cry02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_cry03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_cry04.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_heart01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_idle01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_idle02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_idle03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_jitome01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_kime01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_kirakira01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_angry01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_angry02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_idle01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_idle02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_idle03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_kime01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_kime02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_pale01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_pale02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_sad01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_shy01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_mal_smile01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_pair_angry1_maru_angry1.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_angry2_maru_angry2.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_bsmile1_maru_smile.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_idle_maru_idle.exp3.json +602 -0
- package/assets/characters/miyako/Expressions/exp_pair_jitome_maru_kime1.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_kime_maru_kime.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_serious1_maru_idle3.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_serious_maru_idle.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_serious_maru_sad.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_smile2_maru_kime2.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_smile3_maru_kime1.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_smile_maru_smile.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pair_surprised2_maru_shy.exp3.json +637 -0
- package/assets/characters/miyako/Expressions/exp_pale01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_pale02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_panic01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_puzzled01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_sad01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_sad02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_sad03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_salk01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_serious01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_serious02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_shadow01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_shock01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_shy01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_shy02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_smile01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_smile02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_smile03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_smile04.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_spin01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_surprised01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_surprised02.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_surprised03.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_thinking01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_tired01.exp3.json +1 -0
- package/assets/characters/miyako/Expressions/exp_tired02.exp3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry02_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry02_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_angry02_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_bye01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_bye01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_bye01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_check01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_check01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_check01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_cry01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_cry01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_cry01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_denial01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_denial01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_denial01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle02_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle02_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_idle02_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_kime01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_kime01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_kime01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look02_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look02_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_look02_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_angry01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_angry01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_angry01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_bye01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_bye01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_bye01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_cry01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_cry01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_cry01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_happy01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_happy01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_happy01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_rampage01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_rampage01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_rampage01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_smile01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_smile01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_mal_smile01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod02_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod02_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod02_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod03_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod03_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_nod03_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_question01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_question01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_question01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_sad01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_sad01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_sad01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_serious01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_serious01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_serious01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile02_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile02_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_smile02_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_surprised01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_surprised01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_surprised01_R.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_thinking01_C.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_thinking01_L.motion3.json +1 -0
- package/assets/characters/miyako/Motions/mtn_thinking01_R.motion3.json +1 -0
- package/assets/characters/miyako/Textures/texture_00.webp +0 -0
- package/assets/characters/miyako/Textures/texture_01.webp +0 -0
- package/assets/characters/miyako/Textures/texture_02.webp +0 -0
- package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.moc3 +0 -0
- package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.model3.json +1 -0
- package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.model3.json.bak +1 -0
- package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.physics3.json +1 -0
- package/assets/characters/miyako/model.json +23 -0
- package/assets/characters/viola/Expressions/exp_angry01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_bsmile01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_bsmile02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_cry01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_cry02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_idle01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_idle02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_idle03.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_idle04.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_kime01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_pale01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_pale02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_sad01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_sad02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_serious01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_serious02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_shadow01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_smile01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_smile02.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_smile02_nomouth.exp3.json +330 -0
- package/assets/characters/viola/Expressions/exp_smile03.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_surprised01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_surprised01_nomouth.exp3.json +330 -0
- package/assets/characters/viola/Expressions/exp_thinking01.exp3.json +1 -0
- package/assets/characters/viola/Expressions/exp_thinking01_nomouth.exp3.json +330 -0
- package/assets/characters/viola/Motions/mtn_angry01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_angry01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_angry01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_bye01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_bye01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_bye01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_check01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_check01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_check01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_cry01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_cry01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_cry01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_denial01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_denial01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_denial01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_idle01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_idle01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_idle01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_kime01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_kime01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_kime01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_look01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_look01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_look01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod02_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod02_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_nod02_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_question01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_question01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_question01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_sad01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_sad01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_sad01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_serious01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_serious01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_serious01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile02_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile02_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_smile02_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_surprised01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_surprised01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_surprised01_R.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_thinking01_C.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_thinking01_L.motion3.json +1 -0
- package/assets/characters/viola/Motions/mtn_thinking01_R.motion3.json +1 -0
- package/assets/characters/viola/Textures/texture_00.webp +0 -0
- package/assets/characters/viola/Textures/texture_01.webp +0 -0
- package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.moc3 +0 -0
- package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.model3.json +388 -0
- package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.physics3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_angry01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_bsmile01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_bsmile02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_cry01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_cry02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_idle01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_idle02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_idle03.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_idle04.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_kime01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_pale01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_pale02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_sad01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_sad02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_serious01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_serious02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_shadow01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_smile01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_smile02.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_smile02_nomouth.exp3.json +330 -0
- package/assets/characters/viola_school/Expressions/exp_smile03.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_surprised01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_surprised01_nomouth.exp3.json +330 -0
- package/assets/characters/viola_school/Expressions/exp_thinking01.exp3.json +1 -0
- package/assets/characters/viola_school/Expressions/exp_thinking01_nomouth.exp3.json +330 -0
- package/assets/characters/viola_school/Motions/mtn_angry01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_angry01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_angry01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_bye01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_bye01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_bye01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_check01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_check01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_check01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_cry01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_cry01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_cry01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_denial01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_denial01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_denial01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_idle01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_idle01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_idle01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_kime01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_kime01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_kime01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_look01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_look01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_look01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod02_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod02_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_nod02_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_question01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_question01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_question01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_sad01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_sad01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_sad01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_serious01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_serious01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_serious01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile02_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile02_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_smile02_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_surprised01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_surprised01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_surprised01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_thinking01_C.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_thinking01_L.motion3.json +1 -0
- package/assets/characters/viola_school/Motions/mtn_thinking01_R.motion3.json +1 -0
- package/assets/characters/viola_school/Textures/texture_00.webp +0 -0
- package/assets/characters/viola_school/Textures/texture_01.webp +0 -0
- package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.moc3 +0 -0
- package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.model3.json +388 -0
- package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.physics3.json +1 -0
- package/assets/click/click.mp3 +0 -0
- package/assets/voice/README.txt +139 -0
- package/assets/voice/done-fc.opus +0 -0
- package/assets/voice/done-score.opus +0 -0
- package/assets/voice/idle-common02.opus +0 -0
- package/assets/voice/idle-levelup.opus +0 -0
- package/assets/voice/idle-marukun.opus +0 -0
- package/assets/voice/listening.opus +0 -0
- package/assets/voice/miyako-farewell.opus +0 -0
- package/assets/voice/thinking-comon.opus +0 -0
- package/assets/voice/thinking-gekisou.opus +0 -0
- package/assets/voice/viola-click1.opus +0 -0
- package/assets/voice/viola-click2.opus +0 -0
- package/assets/voice/viola-done1.opus +0 -0
- package/assets/voice/viola-done2.opus +0 -0
- package/assets/voice/viola-done3.opus +0 -0
- package/assets/voice/viola-farewell.opus +0 -0
- package/assets/voice/viola-idle1.opus +0 -0
- package/assets/voice/viola-idle2.opus +0 -0
- package/assets/voice/viola-idle3.opus +0 -0
- package/assets/voice/viola-idle4.opus +0 -0
- package/assets/voice/viola-listening.opus +0 -0
- package/assets/voice/viola-thinking1.opus +0 -0
- package/assets/voice/viola-thinking2.opus +0 -0
- package/assets/voice/viola-thinking3.opus +0 -0
- package/assets/voice/viola-working1.opus +0 -0
- package/assets/voice/viola-working2.opus +0 -0
- package/assets/voice/viola-working3.opus +0 -0
- package/assets/voice/working-combo.opus +0 -0
- package/assets/voice/working-skill.opus +0 -0
- package/assets/voice/working-training.opus +0 -0
- package/cordis.patch.yml +18 -0
- package/lib/client.js +5514 -0
- package/lib/client.live2d.js +67 -0
- package/lib/framing.js +284 -0
- package/lib/index.js +591 -0
- package/lib/machine.js +279 -0
- package/lib/persist.js +183 -0
- package/lib/routes.js +206 -0
- package/package.json +57 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-pet-live2d-viola — a Live2D desktop pet for the DSH web GUI.
|
|
3
|
+
*
|
|
4
|
+
* Host half. Owns everything that must outlive one browser tab: the persisted
|
|
5
|
+
* layout, the session-activity bridge that drives the pet's reactions, and the
|
|
6
|
+
* HTTP surfaces (model assets + a small JSON API) the browser polls.
|
|
7
|
+
*
|
|
8
|
+
* The client half is `lib/client.js`, served by the client-module loader as the
|
|
9
|
+
* package's `./client` export. The Live2D runtime itself is
|
|
10
|
+
* `lib/client.live2d.js`, a package-local chunk the client requests with
|
|
11
|
+
* `require.async("./client.live2d.js")`; the loader serves it from
|
|
12
|
+
* `/plugins/dsh-pet-live2d-viola/` so no host route is needed for it.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-pet-live2d-viola
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import z from "schemastery";
|
|
18
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
19
|
+
import { mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
import { PetMachine, PHASES } from "./machine.js";
|
|
25
|
+
import {
|
|
26
|
+
CHARACTERS_DIR,
|
|
27
|
+
CROP_PRESETS,
|
|
28
|
+
DEFAULT_CROP,
|
|
29
|
+
DEFAULT_MODEL,
|
|
30
|
+
DEFAULT_PET_NAME,
|
|
31
|
+
LAYOUT_LIMITS,
|
|
32
|
+
emptyPersist,
|
|
33
|
+
loadPersist,
|
|
34
|
+
normalizeLayout,
|
|
35
|
+
petHomeDir,
|
|
36
|
+
savePersist
|
|
37
|
+
} from "./persist.js";
|
|
38
|
+
import { makePetRoutes } from "./routes.js";
|
|
39
|
+
|
|
40
|
+
export { PHASES, PetMachine } from "./machine.js";
|
|
41
|
+
export {
|
|
42
|
+
CHARACTERS_DIR,
|
|
43
|
+
CROP_PRESETS,
|
|
44
|
+
DEFAULT_CROP,
|
|
45
|
+
DEFAULT_LAYOUT,
|
|
46
|
+
DEFAULT_MODEL,
|
|
47
|
+
DEFAULT_PET_NAME,
|
|
48
|
+
LAYOUT_LIMITS,
|
|
49
|
+
normalizeLayout
|
|
50
|
+
} from "./persist.js";
|
|
51
|
+
export { PET_API_PREFIX, PET_ASSET_PREFIX } from "./routes.js";
|
|
52
|
+
|
|
53
|
+
/** Display name of the character shown on a fresh install. */
|
|
54
|
+
export const DEFAULT_MODEL_NAME = DEFAULT_MODEL;
|
|
55
|
+
/**
|
|
56
|
+
* Directory (relative to the package root) holding one subdirectory per character.
|
|
57
|
+
*
|
|
58
|
+
* The plugin ships two models and switches between them from the menu, so they live in a tree keyed by
|
|
59
|
+
* character id rather than in one fixed directory.
|
|
60
|
+
*/
|
|
61
|
+
export const MODELS_DIR = CHARACTERS_DIR;
|
|
62
|
+
/**
|
|
63
|
+
* Directory (relative to the package root) holding the bundled voice clips.
|
|
64
|
+
*
|
|
65
|
+
* Kept OUT of the model directory on purpose: the model is addressed as
|
|
66
|
+
* `/pet-live2d-viola/assets/<settingsFile>`, so the asset root has to be the model directory itself.
|
|
67
|
+
*/
|
|
68
|
+
export const VOICE_DIR = join("assets", "voice");
|
|
69
|
+
/**
|
|
70
|
+
* The click sound's directory.
|
|
71
|
+
*
|
|
72
|
+
* Its own tree rather than a file inside the voice directory: the voice clips are the character's spoken
|
|
73
|
+
* lines and are addressed one-per-phase, while this is a UI sound in a different format (`.mp3`). Keeping
|
|
74
|
+
* them apart means "the voice directory" keeps meaning one thing.
|
|
75
|
+
*/
|
|
76
|
+
export const CLICK_DIR = join("assets", "click");
|
|
77
|
+
/** Extensions that make up one Cubism model tree, for asset allow-listing. */
|
|
78
|
+
export const MODEL_FILE_PATTERN = /\.(model3\.json|moc3|physics3\.json|pose3\.json|cdi3\.json|motion3\.json|exp3\.json|userdata3\.json|png|webp|jpe?g|mp3|wav|ogg|txt)$/i;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Motion pools per character, by phase name.
|
|
82
|
+
*
|
|
83
|
+
* A pool is the set of motions a phase may drift through at random. It is per character because the entries
|
|
84
|
+
* name motion groups that exist in one model and not the other: 都子's idle pool is her everyday motions plus
|
|
85
|
+
* all six plush poses, while 薇欧拉 has no plush and no drift chosen for her yet.
|
|
86
|
+
*
|
|
87
|
+
* An EMPTY pool is a supported state — the drift becomes a no-op — which is what lets a character be added
|
|
88
|
+
* with its model and its menu before its choreography is decided.
|
|
89
|
+
*/
|
|
90
|
+
export const CHARACTER_PHASES = {
|
|
91
|
+
miyako: {
|
|
92
|
+
idle: {
|
|
93
|
+
idleMotions: [
|
|
94
|
+
"mtn_idle01_C",
|
|
95
|
+
"mtn_idle02_C",
|
|
96
|
+
"mtn_look01_C",
|
|
97
|
+
"mtn_look02_C",
|
|
98
|
+
"mtn_mal_smile01_C",
|
|
99
|
+
"mtn_mal_happy01_C",
|
|
100
|
+
"mtn_mal_angry01_C",
|
|
101
|
+
"mtn_mal_cry01_C",
|
|
102
|
+
"mtn_mal_bye01_C",
|
|
103
|
+
"mtn_mal_rampage01_C"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
working: {
|
|
107
|
+
randomMotions: ["mtn_serious01_C", "mtn_nod01_C", "mtn_smile01_C"],
|
|
108
|
+
idleMotions: ["mtn_serious01_C", "mtn_nod01_C", "mtn_smile01_C"]
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
viola: {
|
|
112
|
+
idle: {
|
|
113
|
+
idleMotions: ["mtn_idle01_C", "mtn_look01_C", "mtn_smile01_C"]
|
|
114
|
+
},
|
|
115
|
+
working: { randomMotions: [], idleMotions: [] }
|
|
116
|
+
},
|
|
117
|
+
/*
|
|
118
|
+
* 薇欧拉's school uniform: a different MODEL with the same content, so it needs the same pools.
|
|
119
|
+
*
|
|
120
|
+
* Written out rather than shared by reference with the entry above. The two are separate models, and this is
|
|
121
|
+
* the place their choreography would diverge if it ever should.
|
|
122
|
+
*/
|
|
123
|
+
viola_school: {
|
|
124
|
+
idle: {
|
|
125
|
+
idleMotions: ["mtn_idle01_C", "mtn_look01_C", "mtn_smile01_C"]
|
|
126
|
+
},
|
|
127
|
+
working: { randomMotions: [], idleMotions: [] }
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* There is deliberately no label derivation here.
|
|
133
|
+
*
|
|
134
|
+
* An earlier version tried to recover a character name from the settings file name, and it produced
|
|
135
|
+
* `sub_viola_casual_spring` for 薇欧拉's casual outfit — the game's file names put the variant in the middle of the
|
|
136
|
+
* identifier, so the short name cannot be recovered from them. The browser's own table names every model this
|
|
137
|
+
* build ships, and it falls back to the id for one it does not; a second, worse source of the same information is
|
|
138
|
+
* what made the menu show a file name.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/** Absolute package root, resolved from this module's own location (lib/). */
|
|
142
|
+
export function petPackageRoot(importMetaUrl) {
|
|
143
|
+
return fileURLToPath(new URL("../", importMetaUrl));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Read the model's own manifest and report what it can play, so the browser
|
|
148
|
+
* never has to guess motion or expression names.
|
|
149
|
+
* @param modelDir - absolute directory of one model tree.
|
|
150
|
+
* @returns motion groups, expressions and the settings file name.
|
|
151
|
+
*/
|
|
152
|
+
export function readModelManifest(modelDir) {
|
|
153
|
+
const entries = readdirSync(modelDir);
|
|
154
|
+
const settingsFile = entries.find((name) => name.endsWith(".model3.json"));
|
|
155
|
+
if (settingsFile === undefined) throw new Error(`no .model3.json in ${modelDir}`);
|
|
156
|
+
const settings = JSON.parse(readFileSync(join(modelDir, settingsFile), "utf8"));
|
|
157
|
+
const motions = Object.keys(settings.FileReferences?.Motions ?? {});
|
|
158
|
+
const expressions = (settings.FileReferences?.Expressions ?? [])
|
|
159
|
+
.map((entry) => entry?.Name)
|
|
160
|
+
.filter((name) => typeof name === "string" && name !== "");
|
|
161
|
+
const groups = (settings.Groups ?? [])
|
|
162
|
+
.filter((group) => group?.Target === "Parameter" && Array.isArray(group.Ids))
|
|
163
|
+
.map((group) => ({ name: group.Name, ids: group.Ids }));
|
|
164
|
+
return { settingsFile, motions, expressions, groups };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Discover the characters this package ships.
|
|
169
|
+
*
|
|
170
|
+
* One subdirectory per character, identified by its folder name, and only those that actually contain a
|
|
171
|
+
* `.model3.json` count — a stray directory (a leftover extraction, an empty folder) is skipped rather than
|
|
172
|
+
* offered in the menu as an entry that cannot load.
|
|
173
|
+
*
|
|
174
|
+
* The display labels live in the browser, next to the rest of the user-facing copy, so this stays about
|
|
175
|
+
* what exists on disk.
|
|
176
|
+
*
|
|
177
|
+
* @param dir - absolute path of the characters directory.
|
|
178
|
+
* @returns `[{ id }]`, sorted by id so the menu order is stable across platforms.
|
|
179
|
+
*/
|
|
180
|
+
export function discoverCharacters(dir) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
const found = [];
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
if (!entry.isDirectory()) continue;
|
|
190
|
+
try {
|
|
191
|
+
if (readdirSync(join(dir, entry.name)).some((name) => name.endsWith(".model3.json"))) {
|
|
192
|
+
found.push({ id: entry.name });
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// An unreadable directory is not a character.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return found.sort((a, b) => a.id.localeCompare(b.id));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The pet service: persisted layout + the session-driven state machine.
|
|
203
|
+
*
|
|
204
|
+
* Events only ever touch in-memory state; the file is written when the user
|
|
205
|
+
* moves/renames the pet or the plugin config changes.
|
|
206
|
+
*/
|
|
207
|
+
export class PetLive2DService extends Service {
|
|
208
|
+
/** Hard dependency: the browser HTTP carrier that serves assets and the API. */
|
|
209
|
+
static inject = ["webServer"];
|
|
210
|
+
|
|
211
|
+
constructor(ctx, config = {}) {
|
|
212
|
+
/*
|
|
213
|
+
* The service name is namespaced, and that is load-bearing rather than tidiness.
|
|
214
|
+
*
|
|
215
|
+
* cordis refuses a service name that is already registered, and the standalone 都子 plugin registers
|
|
216
|
+
* `petLive2d`. While both packages were installed, this plugin therefore FAILED TO LOAD — the whole pet,
|
|
217
|
+
* not just a feature — and the symptom was simply that the character never appeared, with nothing on
|
|
218
|
+
* screen pointing at the cause.
|
|
219
|
+
*/
|
|
220
|
+
super(ctx, "petLive2dViola");
|
|
221
|
+
this.packageRoot = config.packageRoot ?? petPackageRoot(import.meta.url);
|
|
222
|
+
this.charactersDir = join(this.packageRoot, MODELS_DIR);
|
|
223
|
+
/*
|
|
224
|
+
* Every character the package ships, discovered from disk rather than listed in code: adding a model
|
|
225
|
+
* is then a directory copy, and a character whose tree is missing simply does not appear in the menu
|
|
226
|
+
* instead of becoming a broken entry.
|
|
227
|
+
*/
|
|
228
|
+
this.characters = discoverCharacters(this.charactersDir);
|
|
229
|
+
this.manifests = new Map();
|
|
230
|
+
this.voiceDir = join(this.packageRoot, VOICE_DIR);
|
|
231
|
+
this.clickDir = join(this.packageRoot, CLICK_DIR);
|
|
232
|
+
this.stateDir = config.stateDir ?? petHomeDir(process.env, homedir());
|
|
233
|
+
this.stateFile = join(this.stateDir, "pet-live2d-viola.json");
|
|
234
|
+
this.disabled = config.enabled === false;
|
|
235
|
+
this.affinity = config.affinity === true;
|
|
236
|
+
|
|
237
|
+
const loaded = loadPersist(readFileSync, this.stateFile);
|
|
238
|
+
this.record = loaded ?? emptyPersist();
|
|
239
|
+
/*
|
|
240
|
+
* The stored character is validated HERE, against what is actually on disk. `loadPersist` carries the
|
|
241
|
+
* id through as a plain string because it must stay filesystem-free, so a character that has since
|
|
242
|
+
* been removed would otherwise leave the pet pointing at a missing model.
|
|
243
|
+
*/
|
|
244
|
+
const wanted = config.character ?? this.record.character ?? DEFAULT_MODEL;
|
|
245
|
+
this.modelName = this.characters.some((c) => c.id === wanted)
|
|
246
|
+
? wanted
|
|
247
|
+
: (this.characters.find((c) => c.id === DEFAULT_MODEL)?.id ?? this.characters[0]?.id ?? DEFAULT_MODEL);
|
|
248
|
+
this.record.character = this.modelName;
|
|
249
|
+
this.modelDir = join(this.charactersDir, this.modelName);
|
|
250
|
+
this.manifest = config.manifest ?? this.manifestFor(this.modelName);
|
|
251
|
+
|
|
252
|
+
this.machine = new PetMachine(config.timing ?? {});
|
|
253
|
+
this.applyConfig(config);
|
|
254
|
+
this.syncActivity();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The model manifest for one character, read once and kept.
|
|
259
|
+
*
|
|
260
|
+
* Cached because the menu asks for every character's motions and expressions, and re-reading and
|
|
261
|
+
* re-parsing each `model3.json` on every state poll would be wasted work for a file that cannot change
|
|
262
|
+
* while the process runs.
|
|
263
|
+
*/
|
|
264
|
+
manifestFor(id) {
|
|
265
|
+
const cached = this.manifests.get(id);
|
|
266
|
+
if (cached !== undefined) return cached;
|
|
267
|
+
const manifest = readModelManifest(join(this.charactersDir, id));
|
|
268
|
+
this.manifests.set(id, manifest);
|
|
269
|
+
return manifest;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Switch to another character.
|
|
274
|
+
*
|
|
275
|
+
* Re-points the asset tree as well as the manifest: the model and its textures, motions and expressions are
|
|
276
|
+
* addressed relative to `/pet-live2d-viola/assets/`, so that route has to be re-registered against the new
|
|
277
|
+
* directory or the browser would keep loading the previous model's files.
|
|
278
|
+
*
|
|
279
|
+
* @returns true when the active character changed.
|
|
280
|
+
*/
|
|
281
|
+
setCharacter(id) {
|
|
282
|
+
if (typeof id !== "string" || !this.characters.some((c) => c.id === id)) return false;
|
|
283
|
+
if (id === this.modelName) return false;
|
|
284
|
+
this.modelName = id;
|
|
285
|
+
this.record.character = id;
|
|
286
|
+
this.modelDir = join(this.charactersDir, id);
|
|
287
|
+
this.manifest = this.manifestFor(id);
|
|
288
|
+
this.syncRoutes();
|
|
289
|
+
this.flush();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The characters the menu may offer, with the active one marked. */
|
|
294
|
+
charactersView() {
|
|
295
|
+
return {
|
|
296
|
+
active: this.modelName,
|
|
297
|
+
available: this.characters.map((character) => ({
|
|
298
|
+
id: character.id,
|
|
299
|
+
// The asset sub-path the browser adds to PET_ASSET_PREFIX to reach this model.
|
|
300
|
+
path: character.id,
|
|
301
|
+
motions: this.manifestFor(character.id).motions,
|
|
302
|
+
expressions: this.manifestFor(character.id).expressions
|
|
303
|
+
/*
|
|
304
|
+
* No display label: the browser has a localized name for every model this build knows, and a
|
|
305
|
+
* derived one cannot compete — the game's file names do not contain the character name in a form
|
|
306
|
+
* that can be recovered, so `sub_viola`'s variants came out as `sub_viola_casual_spring`. The id
|
|
307
|
+
* is the browser's fallback for a model it has no translation for, which is the honest outcome.
|
|
308
|
+
*/
|
|
309
|
+
}))
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The model tree the browser should load. */
|
|
314
|
+
manifestView() {
|
|
315
|
+
return {
|
|
316
|
+
name: this.modelName,
|
|
317
|
+
settings: this.manifest.settingsFile,
|
|
318
|
+
motions: this.manifest.motions,
|
|
319
|
+
expressions: this.manifest.expressions,
|
|
320
|
+
groups: this.manifest.groups
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Current layout (defensive copy). */
|
|
325
|
+
layout() {
|
|
326
|
+
return { ...this.record.layout };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Current pet name. */
|
|
330
|
+
petName() {
|
|
331
|
+
return this.record.name;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The full snapshot the browser polls: machine state, layout, the model
|
|
336
|
+
* manifest it needs to load the model, and the animation binding table.
|
|
337
|
+
*/
|
|
338
|
+
view() {
|
|
339
|
+
const state = this.machine.view();
|
|
340
|
+
const definition = PHASES[state.phase] ?? PHASES.idle;
|
|
341
|
+
const pool = CHARACTER_PHASES[this.modelName]?.[state.phase] ?? {};
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
enabled: !this.disabled,
|
|
345
|
+
name: this.record.name,
|
|
346
|
+
model: this.manifestView(),
|
|
347
|
+
/**
|
|
348
|
+
* Every character the menu can switch to, plus which one is active.
|
|
349
|
+
*
|
|
350
|
+
* Sent in the same snapshot as the active model's manifest so the client can build the whole menu
|
|
351
|
+
* — including the labels for characters that are not loaded — without a second request.
|
|
352
|
+
*/
|
|
353
|
+
characters: this.charactersView(),
|
|
354
|
+
layout: { ...this.record.layout },
|
|
355
|
+
phase: state.phase,
|
|
356
|
+
revision: state.revision,
|
|
357
|
+
bubble: state.bubble,
|
|
358
|
+
holding: state.holding,
|
|
359
|
+
sessions: state.sessions,
|
|
360
|
+
/**
|
|
361
|
+
* What to play on entering a phase, and the motions it drifts through at random
|
|
362
|
+
* while the phase lasts. The client mirrors this table. No expression: her face
|
|
363
|
+
* changes only when one is picked from the menu.
|
|
364
|
+
*
|
|
365
|
+
* `randomMotions`, when present, is a set the client picks ONE entry motion from instead
|
|
366
|
+
* of always playing `motion`. It exists because a transient phase can be shorter than the
|
|
367
|
+
* drift interval, which left the drift set unreachable and made every completion identical.
|
|
368
|
+
*/
|
|
369
|
+
animation: {
|
|
370
|
+
motion: definition.motion,
|
|
371
|
+
randomMotions: pool.randomMotions ?? definition.randomMotions,
|
|
372
|
+
idleMotions: pool.idleMotions ?? definition.idleMotions
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The whole phase -> animation table, for diagnostics and for the harness that verifies
|
|
379
|
+
* every phase's clips actually play. `view()` only exposes the current phase, which is all
|
|
380
|
+
* the browser needs but not enough to check the other five.
|
|
381
|
+
*/
|
|
382
|
+
phaseTable(character = this.modelName) {
|
|
383
|
+
const out = {};
|
|
384
|
+
for (const [name, definition] of Object.entries(PHASES)) {
|
|
385
|
+
// The shared table supplies the entry motion and the bubble; this character's pools override the
|
|
386
|
+
// drift sets, which are the part that names motions the other model may not have.
|
|
387
|
+
const override = CHARACTER_PHASES[character]?.[name] ?? {};
|
|
388
|
+
out[name] = {
|
|
389
|
+
motion: definition.motion,
|
|
390
|
+
// Mirrors view(): the set the client picks one entry motion from, when there is one.
|
|
391
|
+
randomMotions: override.randomMotions ?? definition.randomMotions,
|
|
392
|
+
idleMotions: override.idleMotions ?? definition.idleMotions,
|
|
393
|
+
bubble: definition.bubble
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Replace the layout with a validated patch.
|
|
401
|
+
*
|
|
402
|
+
* A `character` key in the SAME patch switches the active model. It rides on this route rather than
|
|
403
|
+
* getting its own because the menu treats "which character" as one more piece of the pet's appearance,
|
|
404
|
+
* and the browser already posts here for every other change of that kind — so a switch costs one request
|
|
405
|
+
* and the reply carries the new manifest.
|
|
406
|
+
*/
|
|
407
|
+
setLayout(patch) {
|
|
408
|
+
const changed = patch !== null && typeof patch === "object" && typeof patch.character === "string"
|
|
409
|
+
? this.setCharacter(patch.character)
|
|
410
|
+
: false;
|
|
411
|
+
this.record = { ...this.record, layout: normalizeLayout(patch, this.record.layout) };
|
|
412
|
+
if (!changed) this.flush();
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
characterChanged: changed,
|
|
416
|
+
character: this.modelName,
|
|
417
|
+
model: this.manifestView(),
|
|
418
|
+
characters: this.charactersView(),
|
|
419
|
+
layout: this.layout()
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Rename the pet (trimmed, 1–24 characters). */
|
|
424
|
+
setName(name) {
|
|
425
|
+
const trimmed = typeof name === "string" ? name.trim() : "";
|
|
426
|
+
if (trimmed === "") return { ok: false, error: "name-empty" };
|
|
427
|
+
if (trimmed.length > 24) return { ok: false, error: "name-too-long" };
|
|
428
|
+
this.record = { ...this.record, name: trimmed };
|
|
429
|
+
this.flush();
|
|
430
|
+
return { ok: true, name: trimmed };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Accept a client-side interaction report. `revision` lets the browser hand
|
|
435
|
+
* back the state it acted on, so a stale report cannot rewind a newer phase.
|
|
436
|
+
*/
|
|
437
|
+
reportClientEvent(body) {
|
|
438
|
+
const kind = body?.kind;
|
|
439
|
+
if (kind !== "pat" && kind !== "poke") return { ok: false, error: "invalid-kind" };
|
|
440
|
+
this.machine.enter(kind === "pat" ? "listening" : "done", Date.now(), { holdMs: 1200, force: true });
|
|
441
|
+
return { ok: true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Apply the plugin Config. Only fields the config actually sets are applied,
|
|
446
|
+
* so an unset field keeps whatever the pet's own UI last stored.
|
|
447
|
+
*/
|
|
448
|
+
applyConfig(config) {
|
|
449
|
+
if (typeof config.name === "string" && config.name.trim() !== "") {
|
|
450
|
+
this.record = { ...this.record, name: config.name.trim().slice(0, 24) };
|
|
451
|
+
}
|
|
452
|
+
this.record = { ...this.record, layout: normalizeLayout(config, this.record.layout) };
|
|
453
|
+
if (typeof config.enabled === "boolean") {
|
|
454
|
+
this.disabled = !config.enabled;
|
|
455
|
+
this.syncActivity();
|
|
456
|
+
}
|
|
457
|
+
this.flush();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Start or stop the session listeners and the asset/API routes. */
|
|
461
|
+
setEnabled(enabled) {
|
|
462
|
+
this.disabled = !enabled;
|
|
463
|
+
this.syncActivity();
|
|
464
|
+
this.syncRoutes();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* (Re)bind the session-activity listeners. One listener reduces every host
|
|
469
|
+
* session event to a phase transition; `activity/status` is kept as a
|
|
470
|
+
* compatibility path for the original working-activity protocol.
|
|
471
|
+
*/
|
|
472
|
+
syncActivity() {
|
|
473
|
+
this.disposeActivity?.();
|
|
474
|
+
this.disposeActivity = undefined;
|
|
475
|
+
if (this.disabled) return;
|
|
476
|
+
const disposers = [
|
|
477
|
+
this.ctx.on("session/event", (session, event) => {
|
|
478
|
+
this.machine.onSessionActive(session?.id);
|
|
479
|
+
this.machine.onSessionEvent(event);
|
|
480
|
+
}),
|
|
481
|
+
this.ctx.on("session/disposed", (session) => {
|
|
482
|
+
this.machine.onSessionDisposed(session?.id);
|
|
483
|
+
}),
|
|
484
|
+
this.ctx.on("app-boot/config-reload", () => {
|
|
485
|
+
// A config edit replaces this service; drop the stale timer.
|
|
486
|
+
this.machine.clearSettle();
|
|
487
|
+
})
|
|
488
|
+
];
|
|
489
|
+
this.disposeActivity = () => {
|
|
490
|
+
for (const dispose of disposers) dispose();
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Register the asset + API routes (idempotent).
|
|
496
|
+
*
|
|
497
|
+
* The asset root is the CHARACTERS DIRECTORY, not the active character's folder, and the browser addresses a
|
|
498
|
+
* model as `<prefix>/<character>/<settings file>`. That keeps every character's textures on distinct URLs,
|
|
499
|
+
* which is required: both models name their textures identically, PIXI caches textures globally by URL, and
|
|
500
|
+
* a flat path made the second model reuse the first one's atlas — it drew as a patchwork figure.
|
|
501
|
+
*
|
|
502
|
+
* Re-registering on every switch is therefore no longer necessary for correctness, but the call is kept so
|
|
503
|
+
* the route is rebuilt after a config change.
|
|
504
|
+
*/
|
|
505
|
+
syncRoutes() {
|
|
506
|
+
this.disposeRoutes?.();
|
|
507
|
+
this.disposeRoutes = undefined;
|
|
508
|
+
if (this.disabled) return;
|
|
509
|
+
const routes = makePetRoutes({ pet: this, assetRoot: this.charactersDir, voiceRoot: this.voiceDir, clickRoot: this.clickDir });
|
|
510
|
+
const disposers = routes.map((route) => this.ctx.webServer.register(route));
|
|
511
|
+
this.disposeRoutes = () => {
|
|
512
|
+
for (const dispose of disposers) dispose();
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Write the record to disk, tolerating a read-only home.
|
|
518
|
+
*
|
|
519
|
+
* `mkdirSync` is wrapped rather than passed directly, because it takes its options as the SECOND argument while the
|
|
520
|
+
* persistence port calls `mkdir(dir, { recursive })`. Passing the raw function meant the option was dropped, the
|
|
521
|
+
* create failed with ENOENT on a fresh install, `savePersist` swallowed the error and returned false, and — since
|
|
522
|
+
* no caller checks the return value — the pet's settings were never written at all.
|
|
523
|
+
*/
|
|
524
|
+
flush() {
|
|
525
|
+
const mkdir = (dir) => mkdirSync(dir, { recursive: true });
|
|
526
|
+
savePersist({ mkdir, writeFile: writeFileSync, rename: renameSync }, this.stateFile, this.stateDir, this.record);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Release timers and routes. */
|
|
530
|
+
dispose() {
|
|
531
|
+
this.disposeActivity?.();
|
|
532
|
+
this.disposeRoutes?.();
|
|
533
|
+
this.machine.dispose();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Stable cordis plugin name (matches the cordis.patch.yml insert id). */
|
|
538
|
+
export const name = "pet-live2d-viola";
|
|
539
|
+
|
|
540
|
+
/** Services required before the pet can mount its surfaces. */
|
|
541
|
+
export const inject = ["webServer"];
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Plugin config: dsh renders this schema as the plugin's settings form. Every
|
|
545
|
+
* layout field is optional on purpose — an unset field leaves whatever the
|
|
546
|
+
* pet's own UI last stored, so editing one field never resets the others.
|
|
547
|
+
*/
|
|
548
|
+
export const Config = z.object({
|
|
549
|
+
enabled: z.boolean().default(true).description("挂载桌宠。关闭后不注册路由与界面。"),
|
|
550
|
+
model: z.string().default(DEFAULT_MODEL).description("默认角色(assets/characters/ 下的目录名)。菜单里可以随时切换。"),
|
|
551
|
+
visible: z.boolean().description("上线 / 下线。"),
|
|
552
|
+
height: z.number().step(1).min(LAYOUT_LIMITS.height.min).max(LAYOUT_LIMITS.height.max).description("桌宠高度(px)。"),
|
|
553
|
+
right: z.number().step(1).min(LAYOUT_LIMITS.inset.min).max(LAYOUT_LIMITS.inset.max).description("距视口右边缘(px)。"),
|
|
554
|
+
bottom: z.number().step(1).min(LAYOUT_LIMITS.inset.min).max(LAYOUT_LIMITS.inset.max).description("距视口下边缘(px)。"),
|
|
555
|
+
locked: z.boolean().description("锁定位置与缩放。"),
|
|
556
|
+
scale: z.number().step(0.05).min(LAYOUT_LIMITS.scale.min).max(LAYOUT_LIMITS.scale.max).description("模型缩放倍率。"),
|
|
557
|
+
opacity: z.number().step(0.05).min(LAYOUT_LIMITS.opacity.min).max(LAYOUT_LIMITS.opacity.max).description("不透明度。"),
|
|
558
|
+
crop: z.union([
|
|
559
|
+
z.const("full").description("全身(默认,不会切到人物)"),
|
|
560
|
+
z.const("threeQuarter").description("四分之三身(含手持玩偶)"),
|
|
561
|
+
z.const("bust").description("半身特写(头肩)")
|
|
562
|
+
]).description("取景范围:显示人物的多少。"),
|
|
563
|
+
showFrame: z.boolean().description("显示取景框边界(虚线,仅作视觉参考,不影响拖动与缩放)。"),
|
|
564
|
+
blink: z.boolean().description("自动眨眼。"),
|
|
565
|
+
gaze: z.boolean().description("视线与头部追随鼠标。"),
|
|
566
|
+
voiceVolume: z.number().step(0.1).min(LAYOUT_LIMITS.voiceVolume.min).max(LAYOUT_LIMITS.voiceVolume.max)
|
|
567
|
+
.description("语音音量,0 为静音(也可在桌宠菜单的「设置 → 语音音量」里调整)。"),
|
|
568
|
+
name: z.string().min(1).max(24).description("桌宠显示名。")
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Mount the pet: build the service, then register its routes. The service owns
|
|
573
|
+
* the route lifetime so `enabled: false` really removes them.
|
|
574
|
+
*
|
|
575
|
+
* A failure here is reported rather than left to propagate silently. When this plugin failed to mount because
|
|
576
|
+
* its service name clashed with another pet's, the only symptom was that no character appeared — no message, no
|
|
577
|
+
* broken panel, nothing to search for. Logging the reason is what turns that into a one-line diagnosis.
|
|
578
|
+
*/
|
|
579
|
+
export function apply(ctx, config = {}) {
|
|
580
|
+
try {
|
|
581
|
+
const service = new PetLive2DService(ctx, config);
|
|
582
|
+
service.syncRoutes();
|
|
583
|
+
ctx.effect(() => () => service.dispose(), "pet-live2d-viola: service");
|
|
584
|
+
return service;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (typeof console !== "undefined" && typeof console.error === "function") {
|
|
587
|
+
console.error("[pet-live2d-viola] failed to mount:", error);
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
}
|