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.
Files changed (363) hide show
  1. package/LICENSE +44 -0
  2. package/README.md +98 -0
  3. package/assets/characters/miyako/Expressions/exp_amazed01.exp3.json +1 -0
  4. package/assets/characters/miyako/Expressions/exp_angry01.exp3.json +1 -0
  5. package/assets/characters/miyako/Expressions/exp_angry02.exp3.json +1 -0
  6. package/assets/characters/miyako/Expressions/exp_angry03.exp3.json +1 -0
  7. package/assets/characters/miyako/Expressions/exp_angry04.exp3.json +1 -0
  8. package/assets/characters/miyako/Expressions/exp_angry05.exp3.json +1 -0
  9. package/assets/characters/miyako/Expressions/exp_anxiety01.exp3.json +1 -0
  10. package/assets/characters/miyako/Expressions/exp_bsmile01.exp3.json +1 -0
  11. package/assets/characters/miyako/Expressions/exp_bsmile02.exp3.json +1 -0
  12. package/assets/characters/miyako/Expressions/exp_cry01.exp3.json +1 -0
  13. package/assets/characters/miyako/Expressions/exp_cry02.exp3.json +1 -0
  14. package/assets/characters/miyako/Expressions/exp_cry03.exp3.json +1 -0
  15. package/assets/characters/miyako/Expressions/exp_cry04.exp3.json +1 -0
  16. package/assets/characters/miyako/Expressions/exp_heart01.exp3.json +1 -0
  17. package/assets/characters/miyako/Expressions/exp_idle01.exp3.json +1 -0
  18. package/assets/characters/miyako/Expressions/exp_idle02.exp3.json +1 -0
  19. package/assets/characters/miyako/Expressions/exp_idle03.exp3.json +1 -0
  20. package/assets/characters/miyako/Expressions/exp_jitome01.exp3.json +1 -0
  21. package/assets/characters/miyako/Expressions/exp_kime01.exp3.json +1 -0
  22. package/assets/characters/miyako/Expressions/exp_kirakira01.exp3.json +1 -0
  23. package/assets/characters/miyako/Expressions/exp_mal_angry01.exp3.json +1 -0
  24. package/assets/characters/miyako/Expressions/exp_mal_angry02.exp3.json +1 -0
  25. package/assets/characters/miyako/Expressions/exp_mal_idle01.exp3.json +1 -0
  26. package/assets/characters/miyako/Expressions/exp_mal_idle02.exp3.json +1 -0
  27. package/assets/characters/miyako/Expressions/exp_mal_idle03.exp3.json +1 -0
  28. package/assets/characters/miyako/Expressions/exp_mal_kime01.exp3.json +1 -0
  29. package/assets/characters/miyako/Expressions/exp_mal_kime02.exp3.json +1 -0
  30. package/assets/characters/miyako/Expressions/exp_mal_pale01.exp3.json +1 -0
  31. package/assets/characters/miyako/Expressions/exp_mal_pale02.exp3.json +1 -0
  32. package/assets/characters/miyako/Expressions/exp_mal_sad01.exp3.json +1 -0
  33. package/assets/characters/miyako/Expressions/exp_mal_shy01.exp3.json +1 -0
  34. package/assets/characters/miyako/Expressions/exp_mal_smile01.exp3.json +1 -0
  35. package/assets/characters/miyako/Expressions/exp_pair_angry1_maru_angry1.exp3.json +637 -0
  36. package/assets/characters/miyako/Expressions/exp_pair_angry2_maru_angry2.exp3.json +637 -0
  37. package/assets/characters/miyako/Expressions/exp_pair_bsmile1_maru_smile.exp3.json +637 -0
  38. package/assets/characters/miyako/Expressions/exp_pair_idle_maru_idle.exp3.json +602 -0
  39. package/assets/characters/miyako/Expressions/exp_pair_jitome_maru_kime1.exp3.json +637 -0
  40. package/assets/characters/miyako/Expressions/exp_pair_kime_maru_kime.exp3.json +637 -0
  41. package/assets/characters/miyako/Expressions/exp_pair_serious1_maru_idle3.exp3.json +637 -0
  42. package/assets/characters/miyako/Expressions/exp_pair_serious_maru_idle.exp3.json +637 -0
  43. package/assets/characters/miyako/Expressions/exp_pair_serious_maru_sad.exp3.json +637 -0
  44. package/assets/characters/miyako/Expressions/exp_pair_smile2_maru_kime2.exp3.json +637 -0
  45. package/assets/characters/miyako/Expressions/exp_pair_smile3_maru_kime1.exp3.json +637 -0
  46. package/assets/characters/miyako/Expressions/exp_pair_smile_maru_smile.exp3.json +637 -0
  47. package/assets/characters/miyako/Expressions/exp_pair_surprised2_maru_shy.exp3.json +637 -0
  48. package/assets/characters/miyako/Expressions/exp_pale01.exp3.json +1 -0
  49. package/assets/characters/miyako/Expressions/exp_pale02.exp3.json +1 -0
  50. package/assets/characters/miyako/Expressions/exp_panic01.exp3.json +1 -0
  51. package/assets/characters/miyako/Expressions/exp_puzzled01.exp3.json +1 -0
  52. package/assets/characters/miyako/Expressions/exp_sad01.exp3.json +1 -0
  53. package/assets/characters/miyako/Expressions/exp_sad02.exp3.json +1 -0
  54. package/assets/characters/miyako/Expressions/exp_sad03.exp3.json +1 -0
  55. package/assets/characters/miyako/Expressions/exp_salk01.exp3.json +1 -0
  56. package/assets/characters/miyako/Expressions/exp_serious01.exp3.json +1 -0
  57. package/assets/characters/miyako/Expressions/exp_serious02.exp3.json +1 -0
  58. package/assets/characters/miyako/Expressions/exp_shadow01.exp3.json +1 -0
  59. package/assets/characters/miyako/Expressions/exp_shock01.exp3.json +1 -0
  60. package/assets/characters/miyako/Expressions/exp_shy01.exp3.json +1 -0
  61. package/assets/characters/miyako/Expressions/exp_shy02.exp3.json +1 -0
  62. package/assets/characters/miyako/Expressions/exp_smile01.exp3.json +1 -0
  63. package/assets/characters/miyako/Expressions/exp_smile02.exp3.json +1 -0
  64. package/assets/characters/miyako/Expressions/exp_smile03.exp3.json +1 -0
  65. package/assets/characters/miyako/Expressions/exp_smile04.exp3.json +1 -0
  66. package/assets/characters/miyako/Expressions/exp_spin01.exp3.json +1 -0
  67. package/assets/characters/miyako/Expressions/exp_surprised01.exp3.json +1 -0
  68. package/assets/characters/miyako/Expressions/exp_surprised02.exp3.json +1 -0
  69. package/assets/characters/miyako/Expressions/exp_surprised03.exp3.json +1 -0
  70. package/assets/characters/miyako/Expressions/exp_thinking01.exp3.json +1 -0
  71. package/assets/characters/miyako/Expressions/exp_tired01.exp3.json +1 -0
  72. package/assets/characters/miyako/Expressions/exp_tired02.exp3.json +1 -0
  73. package/assets/characters/miyako/Motions/mtn_angry01_C.motion3.json +1 -0
  74. package/assets/characters/miyako/Motions/mtn_angry01_L.motion3.json +1 -0
  75. package/assets/characters/miyako/Motions/mtn_angry01_R.motion3.json +1 -0
  76. package/assets/characters/miyako/Motions/mtn_angry02_C.motion3.json +1 -0
  77. package/assets/characters/miyako/Motions/mtn_angry02_L.motion3.json +1 -0
  78. package/assets/characters/miyako/Motions/mtn_angry02_R.motion3.json +1 -0
  79. package/assets/characters/miyako/Motions/mtn_bye01_C.motion3.json +1 -0
  80. package/assets/characters/miyako/Motions/mtn_bye01_L.motion3.json +1 -0
  81. package/assets/characters/miyako/Motions/mtn_bye01_R.motion3.json +1 -0
  82. package/assets/characters/miyako/Motions/mtn_check01_C.motion3.json +1 -0
  83. package/assets/characters/miyako/Motions/mtn_check01_L.motion3.json +1 -0
  84. package/assets/characters/miyako/Motions/mtn_check01_R.motion3.json +1 -0
  85. package/assets/characters/miyako/Motions/mtn_cry01_C.motion3.json +1 -0
  86. package/assets/characters/miyako/Motions/mtn_cry01_L.motion3.json +1 -0
  87. package/assets/characters/miyako/Motions/mtn_cry01_R.motion3.json +1 -0
  88. package/assets/characters/miyako/Motions/mtn_denial01_C.motion3.json +1 -0
  89. package/assets/characters/miyako/Motions/mtn_denial01_L.motion3.json +1 -0
  90. package/assets/characters/miyako/Motions/mtn_denial01_R.motion3.json +1 -0
  91. package/assets/characters/miyako/Motions/mtn_idle01_C.motion3.json +1 -0
  92. package/assets/characters/miyako/Motions/mtn_idle01_L.motion3.json +1 -0
  93. package/assets/characters/miyako/Motions/mtn_idle01_R.motion3.json +1 -0
  94. package/assets/characters/miyako/Motions/mtn_idle02_C.motion3.json +1 -0
  95. package/assets/characters/miyako/Motions/mtn_idle02_L.motion3.json +1 -0
  96. package/assets/characters/miyako/Motions/mtn_idle02_R.motion3.json +1 -0
  97. package/assets/characters/miyako/Motions/mtn_kime01_C.motion3.json +1 -0
  98. package/assets/characters/miyako/Motions/mtn_kime01_L.motion3.json +1 -0
  99. package/assets/characters/miyako/Motions/mtn_kime01_R.motion3.json +1 -0
  100. package/assets/characters/miyako/Motions/mtn_look01_C.motion3.json +1 -0
  101. package/assets/characters/miyako/Motions/mtn_look01_L.motion3.json +1 -0
  102. package/assets/characters/miyako/Motions/mtn_look01_R.motion3.json +1 -0
  103. package/assets/characters/miyako/Motions/mtn_look02_C.motion3.json +1 -0
  104. package/assets/characters/miyako/Motions/mtn_look02_L.motion3.json +1 -0
  105. package/assets/characters/miyako/Motions/mtn_look02_R.motion3.json +1 -0
  106. package/assets/characters/miyako/Motions/mtn_mal_angry01_C.motion3.json +1 -0
  107. package/assets/characters/miyako/Motions/mtn_mal_angry01_L.motion3.json +1 -0
  108. package/assets/characters/miyako/Motions/mtn_mal_angry01_R.motion3.json +1 -0
  109. package/assets/characters/miyako/Motions/mtn_mal_bye01_C.motion3.json +1 -0
  110. package/assets/characters/miyako/Motions/mtn_mal_bye01_L.motion3.json +1 -0
  111. package/assets/characters/miyako/Motions/mtn_mal_bye01_R.motion3.json +1 -0
  112. package/assets/characters/miyako/Motions/mtn_mal_cry01_C.motion3.json +1 -0
  113. package/assets/characters/miyako/Motions/mtn_mal_cry01_L.motion3.json +1 -0
  114. package/assets/characters/miyako/Motions/mtn_mal_cry01_R.motion3.json +1 -0
  115. package/assets/characters/miyako/Motions/mtn_mal_happy01_C.motion3.json +1 -0
  116. package/assets/characters/miyako/Motions/mtn_mal_happy01_L.motion3.json +1 -0
  117. package/assets/characters/miyako/Motions/mtn_mal_happy01_R.motion3.json +1 -0
  118. package/assets/characters/miyako/Motions/mtn_mal_rampage01_C.motion3.json +1 -0
  119. package/assets/characters/miyako/Motions/mtn_mal_rampage01_L.motion3.json +1 -0
  120. package/assets/characters/miyako/Motions/mtn_mal_rampage01_R.motion3.json +1 -0
  121. package/assets/characters/miyako/Motions/mtn_mal_smile01_C.motion3.json +1 -0
  122. package/assets/characters/miyako/Motions/mtn_mal_smile01_L.motion3.json +1 -0
  123. package/assets/characters/miyako/Motions/mtn_mal_smile01_R.motion3.json +1 -0
  124. package/assets/characters/miyako/Motions/mtn_nod01_C.motion3.json +1 -0
  125. package/assets/characters/miyako/Motions/mtn_nod01_L.motion3.json +1 -0
  126. package/assets/characters/miyako/Motions/mtn_nod01_R.motion3.json +1 -0
  127. package/assets/characters/miyako/Motions/mtn_nod02_C.motion3.json +1 -0
  128. package/assets/characters/miyako/Motions/mtn_nod02_L.motion3.json +1 -0
  129. package/assets/characters/miyako/Motions/mtn_nod02_R.motion3.json +1 -0
  130. package/assets/characters/miyako/Motions/mtn_nod03_C.motion3.json +1 -0
  131. package/assets/characters/miyako/Motions/mtn_nod03_L.motion3.json +1 -0
  132. package/assets/characters/miyako/Motions/mtn_nod03_R.motion3.json +1 -0
  133. package/assets/characters/miyako/Motions/mtn_question01_C.motion3.json +1 -0
  134. package/assets/characters/miyako/Motions/mtn_question01_L.motion3.json +1 -0
  135. package/assets/characters/miyako/Motions/mtn_question01_R.motion3.json +1 -0
  136. package/assets/characters/miyako/Motions/mtn_sad01_C.motion3.json +1 -0
  137. package/assets/characters/miyako/Motions/mtn_sad01_L.motion3.json +1 -0
  138. package/assets/characters/miyako/Motions/mtn_sad01_R.motion3.json +1 -0
  139. package/assets/characters/miyako/Motions/mtn_serious01_C.motion3.json +1 -0
  140. package/assets/characters/miyako/Motions/mtn_serious01_L.motion3.json +1 -0
  141. package/assets/characters/miyako/Motions/mtn_serious01_R.motion3.json +1 -0
  142. package/assets/characters/miyako/Motions/mtn_smile01_C.motion3.json +1 -0
  143. package/assets/characters/miyako/Motions/mtn_smile01_L.motion3.json +1 -0
  144. package/assets/characters/miyako/Motions/mtn_smile01_R.motion3.json +1 -0
  145. package/assets/characters/miyako/Motions/mtn_smile02_C.motion3.json +1 -0
  146. package/assets/characters/miyako/Motions/mtn_smile02_L.motion3.json +1 -0
  147. package/assets/characters/miyako/Motions/mtn_smile02_R.motion3.json +1 -0
  148. package/assets/characters/miyako/Motions/mtn_surprised01_C.motion3.json +1 -0
  149. package/assets/characters/miyako/Motions/mtn_surprised01_L.motion3.json +1 -0
  150. package/assets/characters/miyako/Motions/mtn_surprised01_R.motion3.json +1 -0
  151. package/assets/characters/miyako/Motions/mtn_thinking01_C.motion3.json +1 -0
  152. package/assets/characters/miyako/Motions/mtn_thinking01_L.motion3.json +1 -0
  153. package/assets/characters/miyako/Motions/mtn_thinking01_R.motion3.json +1 -0
  154. package/assets/characters/miyako/Textures/texture_00.webp +0 -0
  155. package/assets/characters/miyako/Textures/texture_01.webp +0 -0
  156. package/assets/characters/miyako/Textures/texture_02.webp +0 -0
  157. package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.moc3 +0 -0
  158. package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.model3.json +1 -0
  159. package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.model3.json.bak +1 -0
  160. package/assets/characters/miyako/adv_live2d_miyako_014_casual_spring_01_marukun.physics3.json +1 -0
  161. package/assets/characters/miyako/model.json +23 -0
  162. package/assets/characters/viola/Expressions/exp_angry01.exp3.json +1 -0
  163. package/assets/characters/viola/Expressions/exp_bsmile01.exp3.json +1 -0
  164. package/assets/characters/viola/Expressions/exp_bsmile02.exp3.json +1 -0
  165. package/assets/characters/viola/Expressions/exp_cry01.exp3.json +1 -0
  166. package/assets/characters/viola/Expressions/exp_cry02.exp3.json +1 -0
  167. package/assets/characters/viola/Expressions/exp_idle01.exp3.json +1 -0
  168. package/assets/characters/viola/Expressions/exp_idle02.exp3.json +1 -0
  169. package/assets/characters/viola/Expressions/exp_idle03.exp3.json +1 -0
  170. package/assets/characters/viola/Expressions/exp_idle04.exp3.json +1 -0
  171. package/assets/characters/viola/Expressions/exp_kime01.exp3.json +1 -0
  172. package/assets/characters/viola/Expressions/exp_pale01.exp3.json +1 -0
  173. package/assets/characters/viola/Expressions/exp_pale02.exp3.json +1 -0
  174. package/assets/characters/viola/Expressions/exp_sad01.exp3.json +1 -0
  175. package/assets/characters/viola/Expressions/exp_sad02.exp3.json +1 -0
  176. package/assets/characters/viola/Expressions/exp_serious01.exp3.json +1 -0
  177. package/assets/characters/viola/Expressions/exp_serious02.exp3.json +1 -0
  178. package/assets/characters/viola/Expressions/exp_shadow01.exp3.json +1 -0
  179. package/assets/characters/viola/Expressions/exp_smile01.exp3.json +1 -0
  180. package/assets/characters/viola/Expressions/exp_smile02.exp3.json +1 -0
  181. package/assets/characters/viola/Expressions/exp_smile02_nomouth.exp3.json +330 -0
  182. package/assets/characters/viola/Expressions/exp_smile03.exp3.json +1 -0
  183. package/assets/characters/viola/Expressions/exp_surprised01.exp3.json +1 -0
  184. package/assets/characters/viola/Expressions/exp_surprised01_nomouth.exp3.json +330 -0
  185. package/assets/characters/viola/Expressions/exp_thinking01.exp3.json +1 -0
  186. package/assets/characters/viola/Expressions/exp_thinking01_nomouth.exp3.json +330 -0
  187. package/assets/characters/viola/Motions/mtn_angry01_C.motion3.json +1 -0
  188. package/assets/characters/viola/Motions/mtn_angry01_L.motion3.json +1 -0
  189. package/assets/characters/viola/Motions/mtn_angry01_R.motion3.json +1 -0
  190. package/assets/characters/viola/Motions/mtn_bye01_C.motion3.json +1 -0
  191. package/assets/characters/viola/Motions/mtn_bye01_L.motion3.json +1 -0
  192. package/assets/characters/viola/Motions/mtn_bye01_R.motion3.json +1 -0
  193. package/assets/characters/viola/Motions/mtn_check01_C.motion3.json +1 -0
  194. package/assets/characters/viola/Motions/mtn_check01_L.motion3.json +1 -0
  195. package/assets/characters/viola/Motions/mtn_check01_R.motion3.json +1 -0
  196. package/assets/characters/viola/Motions/mtn_cry01_C.motion3.json +1 -0
  197. package/assets/characters/viola/Motions/mtn_cry01_L.motion3.json +1 -0
  198. package/assets/characters/viola/Motions/mtn_cry01_R.motion3.json +1 -0
  199. package/assets/characters/viola/Motions/mtn_denial01_C.motion3.json +1 -0
  200. package/assets/characters/viola/Motions/mtn_denial01_L.motion3.json +1 -0
  201. package/assets/characters/viola/Motions/mtn_denial01_R.motion3.json +1 -0
  202. package/assets/characters/viola/Motions/mtn_idle01_C.motion3.json +1 -0
  203. package/assets/characters/viola/Motions/mtn_idle01_L.motion3.json +1 -0
  204. package/assets/characters/viola/Motions/mtn_idle01_R.motion3.json +1 -0
  205. package/assets/characters/viola/Motions/mtn_kime01_C.motion3.json +1 -0
  206. package/assets/characters/viola/Motions/mtn_kime01_L.motion3.json +1 -0
  207. package/assets/characters/viola/Motions/mtn_kime01_R.motion3.json +1 -0
  208. package/assets/characters/viola/Motions/mtn_look01_C.motion3.json +1 -0
  209. package/assets/characters/viola/Motions/mtn_look01_L.motion3.json +1 -0
  210. package/assets/characters/viola/Motions/mtn_look01_R.motion3.json +1 -0
  211. package/assets/characters/viola/Motions/mtn_nod01_C.motion3.json +1 -0
  212. package/assets/characters/viola/Motions/mtn_nod01_L.motion3.json +1 -0
  213. package/assets/characters/viola/Motions/mtn_nod01_R.motion3.json +1 -0
  214. package/assets/characters/viola/Motions/mtn_nod02_C.motion3.json +1 -0
  215. package/assets/characters/viola/Motions/mtn_nod02_L.motion3.json +1 -0
  216. package/assets/characters/viola/Motions/mtn_nod02_R.motion3.json +1 -0
  217. package/assets/characters/viola/Motions/mtn_question01_C.motion3.json +1 -0
  218. package/assets/characters/viola/Motions/mtn_question01_L.motion3.json +1 -0
  219. package/assets/characters/viola/Motions/mtn_question01_R.motion3.json +1 -0
  220. package/assets/characters/viola/Motions/mtn_sad01_C.motion3.json +1 -0
  221. package/assets/characters/viola/Motions/mtn_sad01_L.motion3.json +1 -0
  222. package/assets/characters/viola/Motions/mtn_sad01_R.motion3.json +1 -0
  223. package/assets/characters/viola/Motions/mtn_serious01_C.motion3.json +1 -0
  224. package/assets/characters/viola/Motions/mtn_serious01_L.motion3.json +1 -0
  225. package/assets/characters/viola/Motions/mtn_serious01_R.motion3.json +1 -0
  226. package/assets/characters/viola/Motions/mtn_smile01_C.motion3.json +1 -0
  227. package/assets/characters/viola/Motions/mtn_smile01_L.motion3.json +1 -0
  228. package/assets/characters/viola/Motions/mtn_smile01_R.motion3.json +1 -0
  229. package/assets/characters/viola/Motions/mtn_smile02_C.motion3.json +1 -0
  230. package/assets/characters/viola/Motions/mtn_smile02_L.motion3.json +1 -0
  231. package/assets/characters/viola/Motions/mtn_smile02_R.motion3.json +1 -0
  232. package/assets/characters/viola/Motions/mtn_surprised01_C.motion3.json +1 -0
  233. package/assets/characters/viola/Motions/mtn_surprised01_L.motion3.json +1 -0
  234. package/assets/characters/viola/Motions/mtn_surprised01_R.motion3.json +1 -0
  235. package/assets/characters/viola/Motions/mtn_thinking01_C.motion3.json +1 -0
  236. package/assets/characters/viola/Motions/mtn_thinking01_L.motion3.json +1 -0
  237. package/assets/characters/viola/Motions/mtn_thinking01_R.motion3.json +1 -0
  238. package/assets/characters/viola/Textures/texture_00.webp +0 -0
  239. package/assets/characters/viola/Textures/texture_01.webp +0 -0
  240. package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.moc3 +0 -0
  241. package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.model3.json +388 -0
  242. package/assets/characters/viola/adv_live2d_sub_viola_casual_spring_01.physics3.json +1 -0
  243. package/assets/characters/viola_school/Expressions/exp_angry01.exp3.json +1 -0
  244. package/assets/characters/viola_school/Expressions/exp_bsmile01.exp3.json +1 -0
  245. package/assets/characters/viola_school/Expressions/exp_bsmile02.exp3.json +1 -0
  246. package/assets/characters/viola_school/Expressions/exp_cry01.exp3.json +1 -0
  247. package/assets/characters/viola_school/Expressions/exp_cry02.exp3.json +1 -0
  248. package/assets/characters/viola_school/Expressions/exp_idle01.exp3.json +1 -0
  249. package/assets/characters/viola_school/Expressions/exp_idle02.exp3.json +1 -0
  250. package/assets/characters/viola_school/Expressions/exp_idle03.exp3.json +1 -0
  251. package/assets/characters/viola_school/Expressions/exp_idle04.exp3.json +1 -0
  252. package/assets/characters/viola_school/Expressions/exp_kime01.exp3.json +1 -0
  253. package/assets/characters/viola_school/Expressions/exp_pale01.exp3.json +1 -0
  254. package/assets/characters/viola_school/Expressions/exp_pale02.exp3.json +1 -0
  255. package/assets/characters/viola_school/Expressions/exp_sad01.exp3.json +1 -0
  256. package/assets/characters/viola_school/Expressions/exp_sad02.exp3.json +1 -0
  257. package/assets/characters/viola_school/Expressions/exp_serious01.exp3.json +1 -0
  258. package/assets/characters/viola_school/Expressions/exp_serious02.exp3.json +1 -0
  259. package/assets/characters/viola_school/Expressions/exp_shadow01.exp3.json +1 -0
  260. package/assets/characters/viola_school/Expressions/exp_smile01.exp3.json +1 -0
  261. package/assets/characters/viola_school/Expressions/exp_smile02.exp3.json +1 -0
  262. package/assets/characters/viola_school/Expressions/exp_smile02_nomouth.exp3.json +330 -0
  263. package/assets/characters/viola_school/Expressions/exp_smile03.exp3.json +1 -0
  264. package/assets/characters/viola_school/Expressions/exp_surprised01.exp3.json +1 -0
  265. package/assets/characters/viola_school/Expressions/exp_surprised01_nomouth.exp3.json +330 -0
  266. package/assets/characters/viola_school/Expressions/exp_thinking01.exp3.json +1 -0
  267. package/assets/characters/viola_school/Expressions/exp_thinking01_nomouth.exp3.json +330 -0
  268. package/assets/characters/viola_school/Motions/mtn_angry01_C.motion3.json +1 -0
  269. package/assets/characters/viola_school/Motions/mtn_angry01_L.motion3.json +1 -0
  270. package/assets/characters/viola_school/Motions/mtn_angry01_R.motion3.json +1 -0
  271. package/assets/characters/viola_school/Motions/mtn_bye01_C.motion3.json +1 -0
  272. package/assets/characters/viola_school/Motions/mtn_bye01_L.motion3.json +1 -0
  273. package/assets/characters/viola_school/Motions/mtn_bye01_R.motion3.json +1 -0
  274. package/assets/characters/viola_school/Motions/mtn_check01_C.motion3.json +1 -0
  275. package/assets/characters/viola_school/Motions/mtn_check01_L.motion3.json +1 -0
  276. package/assets/characters/viola_school/Motions/mtn_check01_R.motion3.json +1 -0
  277. package/assets/characters/viola_school/Motions/mtn_cry01_C.motion3.json +1 -0
  278. package/assets/characters/viola_school/Motions/mtn_cry01_L.motion3.json +1 -0
  279. package/assets/characters/viola_school/Motions/mtn_cry01_R.motion3.json +1 -0
  280. package/assets/characters/viola_school/Motions/mtn_denial01_C.motion3.json +1 -0
  281. package/assets/characters/viola_school/Motions/mtn_denial01_L.motion3.json +1 -0
  282. package/assets/characters/viola_school/Motions/mtn_denial01_R.motion3.json +1 -0
  283. package/assets/characters/viola_school/Motions/mtn_idle01_C.motion3.json +1 -0
  284. package/assets/characters/viola_school/Motions/mtn_idle01_L.motion3.json +1 -0
  285. package/assets/characters/viola_school/Motions/mtn_idle01_R.motion3.json +1 -0
  286. package/assets/characters/viola_school/Motions/mtn_kime01_C.motion3.json +1 -0
  287. package/assets/characters/viola_school/Motions/mtn_kime01_L.motion3.json +1 -0
  288. package/assets/characters/viola_school/Motions/mtn_kime01_R.motion3.json +1 -0
  289. package/assets/characters/viola_school/Motions/mtn_look01_C.motion3.json +1 -0
  290. package/assets/characters/viola_school/Motions/mtn_look01_L.motion3.json +1 -0
  291. package/assets/characters/viola_school/Motions/mtn_look01_R.motion3.json +1 -0
  292. package/assets/characters/viola_school/Motions/mtn_nod01_C.motion3.json +1 -0
  293. package/assets/characters/viola_school/Motions/mtn_nod01_L.motion3.json +1 -0
  294. package/assets/characters/viola_school/Motions/mtn_nod01_R.motion3.json +1 -0
  295. package/assets/characters/viola_school/Motions/mtn_nod02_C.motion3.json +1 -0
  296. package/assets/characters/viola_school/Motions/mtn_nod02_L.motion3.json +1 -0
  297. package/assets/characters/viola_school/Motions/mtn_nod02_R.motion3.json +1 -0
  298. package/assets/characters/viola_school/Motions/mtn_question01_C.motion3.json +1 -0
  299. package/assets/characters/viola_school/Motions/mtn_question01_L.motion3.json +1 -0
  300. package/assets/characters/viola_school/Motions/mtn_question01_R.motion3.json +1 -0
  301. package/assets/characters/viola_school/Motions/mtn_sad01_C.motion3.json +1 -0
  302. package/assets/characters/viola_school/Motions/mtn_sad01_L.motion3.json +1 -0
  303. package/assets/characters/viola_school/Motions/mtn_sad01_R.motion3.json +1 -0
  304. package/assets/characters/viola_school/Motions/mtn_serious01_C.motion3.json +1 -0
  305. package/assets/characters/viola_school/Motions/mtn_serious01_L.motion3.json +1 -0
  306. package/assets/characters/viola_school/Motions/mtn_serious01_R.motion3.json +1 -0
  307. package/assets/characters/viola_school/Motions/mtn_smile01_C.motion3.json +1 -0
  308. package/assets/characters/viola_school/Motions/mtn_smile01_L.motion3.json +1 -0
  309. package/assets/characters/viola_school/Motions/mtn_smile01_R.motion3.json +1 -0
  310. package/assets/characters/viola_school/Motions/mtn_smile02_C.motion3.json +1 -0
  311. package/assets/characters/viola_school/Motions/mtn_smile02_L.motion3.json +1 -0
  312. package/assets/characters/viola_school/Motions/mtn_smile02_R.motion3.json +1 -0
  313. package/assets/characters/viola_school/Motions/mtn_surprised01_C.motion3.json +1 -0
  314. package/assets/characters/viola_school/Motions/mtn_surprised01_L.motion3.json +1 -0
  315. package/assets/characters/viola_school/Motions/mtn_surprised01_R.motion3.json +1 -0
  316. package/assets/characters/viola_school/Motions/mtn_thinking01_C.motion3.json +1 -0
  317. package/assets/characters/viola_school/Motions/mtn_thinking01_L.motion3.json +1 -0
  318. package/assets/characters/viola_school/Motions/mtn_thinking01_R.motion3.json +1 -0
  319. package/assets/characters/viola_school/Textures/texture_00.webp +0 -0
  320. package/assets/characters/viola_school/Textures/texture_01.webp +0 -0
  321. package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.moc3 +0 -0
  322. package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.model3.json +388 -0
  323. package/assets/characters/viola_school/adv_live2d_sub_viola_school_winter_hs.physics3.json +1 -0
  324. package/assets/click/click.mp3 +0 -0
  325. package/assets/voice/README.txt +139 -0
  326. package/assets/voice/done-fc.opus +0 -0
  327. package/assets/voice/done-score.opus +0 -0
  328. package/assets/voice/idle-common02.opus +0 -0
  329. package/assets/voice/idle-levelup.opus +0 -0
  330. package/assets/voice/idle-marukun.opus +0 -0
  331. package/assets/voice/listening.opus +0 -0
  332. package/assets/voice/miyako-farewell.opus +0 -0
  333. package/assets/voice/thinking-comon.opus +0 -0
  334. package/assets/voice/thinking-gekisou.opus +0 -0
  335. package/assets/voice/viola-click1.opus +0 -0
  336. package/assets/voice/viola-click2.opus +0 -0
  337. package/assets/voice/viola-done1.opus +0 -0
  338. package/assets/voice/viola-done2.opus +0 -0
  339. package/assets/voice/viola-done3.opus +0 -0
  340. package/assets/voice/viola-farewell.opus +0 -0
  341. package/assets/voice/viola-idle1.opus +0 -0
  342. package/assets/voice/viola-idle2.opus +0 -0
  343. package/assets/voice/viola-idle3.opus +0 -0
  344. package/assets/voice/viola-idle4.opus +0 -0
  345. package/assets/voice/viola-listening.opus +0 -0
  346. package/assets/voice/viola-thinking1.opus +0 -0
  347. package/assets/voice/viola-thinking2.opus +0 -0
  348. package/assets/voice/viola-thinking3.opus +0 -0
  349. package/assets/voice/viola-working1.opus +0 -0
  350. package/assets/voice/viola-working2.opus +0 -0
  351. package/assets/voice/viola-working3.opus +0 -0
  352. package/assets/voice/working-combo.opus +0 -0
  353. package/assets/voice/working-skill.opus +0 -0
  354. package/assets/voice/working-training.opus +0 -0
  355. package/cordis.patch.yml +18 -0
  356. package/lib/client.js +5514 -0
  357. package/lib/client.live2d.js +67 -0
  358. package/lib/framing.js +284 -0
  359. package/lib/index.js +591 -0
  360. package/lib/machine.js +279 -0
  361. package/lib/persist.js +183 -0
  362. package/lib/routes.js +206 -0
  363. 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
+ }