Skip to content

Commit 786c3fd

Browse files
authored
Merge pull request #61 from Rhystic1/integrated
Added Story/Roleplaying Generator feature
2 parents 9b1663c + 6f64421 commit 786c3fd

File tree

14 files changed

+3377
-27
lines changed

14 files changed

+3377
-27
lines changed

original_Loader.vue

41.3 KB
Binary file not shown.

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"crypto-js": "^4.2.0",
2525
"html2canvas": "^1.4.1",
2626
"less": "^4.2.0",
27+
"marked": "^17.0.1",
2728
"naive-ui": "^2.34.4",
2829
"ndarray-pixels": "^3.1.0",
2930
"pinia": "^2.1.6",

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const checkMobile = () => {
2727
return market.globalParams.isMobile
2828
}
2929
const isL2d = () => {
30-
return market.route.name === 'visualiser'
30+
return market.route.name === 'visualiser' || market.route.name === 'story-gen'
3131
}
3232
const isChibiMobile = () => {
3333
return checkMobile() && market.route.name === 'chibi'

src/components/common/Header/routes2Display.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export const ROUTES: route2DisplayInterface[] = [
2424
path: 'gallery',
2525
text: 'Gallery',
2626
mobile: true
27+
},
28+
{
29+
path: 'story-gen',
30+
text: 'Story/Roleplaying Generator',
31+
mobile: true
2732
}
2833
// {
2934
// path: 'tierlistmaker',

src/components/common/Spine/Loader.vue

Lines changed: 219 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div
33
id="player-container"
44
:class="checkMobile() ? 'mobile' : 'computer'"
5+
:style="{ visibility: market.live2d.isVisible ? 'visible' : 'hidden', opacity: market.live2d.isVisible ? 1 : 0 }"
56
></div>
67
</template>
78

@@ -16,9 +17,11 @@ import spine41 from '@/utils/spine/spine-player4.1'
1617
1718
import { globalParams, messagesEnum } from '@/utils/enum/globalParams'
1819
import type { AttachmentInterface, AttachmentItemColorInterface } from '@/utils/interfaces/live2d'
20+
import { animationMappings } from '@/utils/animationMappings'
1921
2022
let canvas: HTMLCanvasElement | null = null
2123
let spineCanvas: any = null
24+
let currentLoadId = 0 // Track active load requests
2225
const market = useMarket()
2326
2427
// http://esotericsoftware.com/spine-player#Viewports
@@ -37,14 +40,135 @@ onMounted(() => {
3740
const SPINE_DEFAULT_MIX = 0.25
3841
let spinePlayer: any = null
3942
43+
const resetAttachmentColors = (player: any) => {
44+
if (!player?.animationState?.data?.skeletonData?.defaultSkin?.attachments) return
45+
46+
player.animationState.data.skeletonData.defaultSkin.attachments.forEach((a: any[]) => {
47+
if (a) {
48+
const keys = Object.keys(a)
49+
if (keys !== null && keys !== undefined && keys.length > 0) {
50+
keys.forEach((k: string) => {
51+
a[k as any].color = {
52+
r: 1,
53+
g: 1,
54+
b: 1,
55+
a: 1
56+
}
57+
})
58+
}
59+
}
60+
})
61+
}
62+
63+
const resolveAnimation = (requested: string, available: string[]): string | null => {
64+
console.log(`[Loader] Resolving animation: '${requested}' against available:`, available)
65+
66+
if (!requested || requested === 'none') return null
67+
if (available.includes(requested)) {
68+
console.log(`[Loader] Found exact match: ${requested}`)
69+
return requested
70+
}
71+
72+
const lowerRequested = requested.toLowerCase()
73+
74+
// Special handling for multi-stage anger (e.g. Chime)
75+
const specialMappings = [
76+
{
77+
target: 'angry',
78+
condition: (avail: string[]) => avail.filter((a) => a.toLowerCase().includes('angry')).length > 1,
79+
triggers: ['irritated', 'bothered', 'grumpy', 'frustrated', 'annoyed', 'displeased']
80+
},
81+
{
82+
target: 'angry_02',
83+
condition: (avail: string[]) => avail.includes('angry_02'),
84+
triggers: ['very angry', 'furious', 'rage', 'shouting', 'yelling', 'livid', 'outraged', 'irate', 'mad']
85+
},
86+
{
87+
target: 'angry_03',
88+
condition: (avail: string[]) => avail.includes('angry_03'),
89+
triggers: ['stern', 'frown', 'slightly angry', 'serious', 'disapproving', 'cold', 'glaring']
90+
}
91+
]
92+
93+
for (const { target, condition, triggers } of specialMappings) {
94+
if (condition(available) && triggers.some((t) => lowerRequested.includes(t))) {
95+
console.log(`[Loader] Mapped '${requested}' to '${target}'`)
96+
return target
97+
}
98+
}
99+
100+
// Direct fuzzy match
101+
const directMatch = available.find((a) => a.toLowerCase().includes(lowerRequested))
102+
if (directMatch) {
103+
console.log(`[Loader] Found direct fuzzy match: ${directMatch}`)
104+
return directMatch
105+
}
106+
107+
// Semantic mapping
108+
for (const [targetAnim, triggers] of Object.entries(animationMappings)) {
109+
// If requested animation contains the target name OR any of the triggers
110+
if (lowerRequested.includes(targetAnim) || triggers.some((t) => lowerRequested.includes(t))) {
111+
112+
// Try to find the target animation in available
113+
// exact match of targetAnim (fuzzy)...
114+
let match = available.find((a) => a.toLowerCase().includes(targetAnim))
115+
if (match) {
116+
console.log(`[Loader] Found semantic match for ${targetAnim} (base): ${match}`)
117+
return match
118+
}
119+
120+
// ...or match any of the triggers in available
121+
for (const trigger of triggers) {
122+
match = available.find((a) => a.toLowerCase().includes(trigger))
123+
if (match) {
124+
console.log(`[Loader] Found semantic match for ${targetAnim} (trigger: ${trigger}): ${match}`)
125+
return match
126+
}
127+
}
128+
}
129+
}
130+
131+
console.warn(`[Loader] No match found for animation: ${requested}`)
132+
return null
133+
}
134+
135+
watch(() => market.live2d.current_animation, (newAnim) => {
136+
if (spinePlayer && newAnim) {
137+
try {
138+
const resolvedAnim = resolveAnimation(newAnim, market.live2d.animations)
139+
140+
if (resolvedAnim) {
141+
spinePlayer.animationState.setAnimation(0, resolvedAnim, true)
142+
} else {
143+
console.warn(`Animation ${newAnim} not found and no fallback discovered.`)
144+
}
145+
} catch (e) {
146+
console.error('Error setting animation:', e)
147+
}
148+
}
149+
})
150+
40151
const spineLoader = () => {
152+
if (!market.live2d.current_id) {
153+
console.log('[Loader] No current_id set, skipping load.')
154+
return
155+
}
156+
157+
currentLoadId++
158+
const thisLoadId = currentLoadId
159+
41160
const skelUrl = getPathing('skel')
42161
const request = new XMLHttpRequest()
43162
44163
request.responseType = 'arraybuffer'
45164
request.open('GET', skelUrl, true)
46165
request.send()
47166
request.onloadend = () => {
167+
if (thisLoadId !== currentLoadId) {
168+
console.log('[Loader] Ignoring stale load request')
169+
return
170+
}
171+
48172
if (request.status !== 200) {
49173
console.error('Failed to load skel file:', request.statusText)
50174
return
@@ -85,6 +209,7 @@ const spineLoader = () => {
85209
atlasUrl: getPathing('atlas'),
86210
animation: getDefaultAnimation(),
87211
skin: market.live2d.getSkin(),
212+
showControls: !market.live2d.hideUI && market.route.name !== 'story-gen',
88213
backgroundColor: '#00000000',
89214
alpha: true,
90215
premultipliedAlpha: true,
@@ -95,24 +220,42 @@ const spineLoader = () => {
95220
defaultMix: SPINE_DEFAULT_MIX,
96221
success: (player: any) => {
97222
98-
spineCanvas.animationState.data.skeletonData.defaultSkin.attachments.forEach((a: any[]) => {
99-
if (a) {
100-
const keys = Object.keys(a)
101-
if (keys !== null && keys !== undefined && keys.length > 0) {
102-
keys.forEach((k: string) => {
103-
a[k as any].color = {
104-
r: 1,
105-
g: 1,
106-
b: 1,
107-
a: 1
108-
}
109-
})
110-
}
111-
}
112-
})
113-
114223
spinePlayer = player
224+
resetAttachmentColors(player)
115225
market.live2d.attachments = player.animationState.data.skeletonData.defaultSkin.attachments
226+
market.live2d.animations = player.animationState.data.skeletonData.animations.map((a: any) => a.name)
227+
228+
const currentAnim = market.live2d.current_animation
229+
let resolvedAnim = resolveAnimation(currentAnim, market.live2d.animations)
230+
231+
if (!resolvedAnim) {
232+
// Try default animation from config
233+
resolvedAnim = resolveAnimation(player.config.animation, market.live2d.animations)
234+
}
235+
236+
if (!resolvedAnim && market.live2d.animations.length > 0) {
237+
// Fallback to first available animation
238+
resolvedAnim = market.live2d.animations[0]
239+
console.warn(`No valid animation found. Falling back to first available: ${resolvedAnim}`)
240+
}
241+
242+
if (resolvedAnim) {
243+
console.log(`[Loader] Setting initial animation to: ${resolvedAnim} (Requested: ${currentAnim})`)
244+
market.live2d.current_animation = resolvedAnim
245+
246+
// Force set animation with a slight delay to ensure player is ready
247+
setTimeout(() => {
248+
try {
249+
player.animationState.setAnimation(0, resolvedAnim, true)
250+
player.play()
251+
} catch (e) {
252+
console.error('[Loader] Failed to set animation in timeout', e)
253+
}
254+
}, 100)
255+
} else {
256+
console.error('[Loader] No animations available for this character.')
257+
}
258+
116259
market.live2d.triggerFinishedLoading()
117260
successfullyLoaded()
118261
},
@@ -156,7 +299,19 @@ const customSpineLoader = () => {
156299
defaultMix: SPINE_DEFAULT_MIX,
157300
success: (player: any) => {
158301
spinePlayer = player
302+
resetAttachmentColors(player)
159303
market.live2d.attachments = player.animationState.data.skeletonData.defaultSkin.attachments
304+
market.live2d.animations = player.animationState.data.skeletonData.animations.map((a: any) => a.name)
305+
306+
const currentAnim = market.live2d.current_animation
307+
const hasAnim = market.live2d.animations.includes(currentAnim)
308+
309+
if (hasAnim) {
310+
player.animationState.setAnimation(0, currentAnim, true)
311+
} else {
312+
market.live2d.current_animation = player.config.animation
313+
}
314+
160315
market.live2d.triggerFinishedLoading()
161316
successfullyLoaded()
162317
try {
@@ -307,15 +462,23 @@ watch(() => market.live2d.exportAnimationTimestamp, (newVal, oldVal) => {
307462
})
308463
309464
watch(() => market.live2d.customLoad, () => {
310-
spineCanvas.dispose()
465+
if (spineCanvas) {
466+
try {
467+
spineCanvas.dispose()
468+
} catch (e) {
469+
console.warn('[Loader] Error disposing spineCanvas for customLoad:', e)
470+
}
471+
spineCanvas = null
472+
}
311473
market.load.beginLoad()
312474
customSpineLoader()
313475
applyDefaultStyle2Canvas()
314476
})
315477
316478
watch(() => market.live2d.hideUI, () => {
317479
const controls = document.querySelector('.spine-player-controls') as HTMLElement
318-
if (market.live2d.hideUI === false) {
480+
if (!controls) return
481+
if (market.live2d.hideUI === false && market.route.name !== 'story-gen') {
319482
controls.style.visibility = 'visible'
320483
} else {
321484
controls.style.visibility = 'hidden'
@@ -442,7 +605,14 @@ async function exportAnimationFrames(timestamp: number) {
442605
443606
const loadSpineAfterWatcher = () => {
444607
if (market.live2d.canLoadSpine) {
445-
spineCanvas.dispose()
608+
if (spineCanvas) {
609+
try {
610+
spineCanvas.dispose()
611+
} catch (e) {
612+
console.warn('[Loader] Error disposing spineCanvas:', e)
613+
}
614+
spineCanvas = null
615+
}
446616
market.load.beginLoad()
447617
spineLoader()
448618
applyDefaultStyle2Canvas()
@@ -531,8 +701,8 @@ document.addEventListener('mousemove', (e) => {
531701
const newX = e.clientX
532702
const newY = e.clientY
533703
534-
const stylel = parseInt(canvas.style.left.replaceAll('px', ''))
535-
const stylet = parseInt(canvas.style.top.replaceAll('px', ''))
704+
const stylel = parseInt(canvas.style.left.replace(/px/g, ''))
705+
const stylet = parseInt(canvas.style.top.replace(/px/g, ''))
536706
537707
if (newX !== oldX) {
538708
canvas.style.left = stylel + (newX - oldX) + 'px'
@@ -604,20 +774,43 @@ const checkIfAssetCanYap = () => {
604774
})
605775
}
606776
setYappable(yappable)
777+
778+
if (yappable && market.live2d.isYapping && market.live2d.yapEnabled) {
779+
try {
780+
spineCanvas.animationState.setAnimation(1, YAP_TRACK, true)
781+
} catch (e) {
782+
console.warn('Could not add yap track on load', e)
783+
}
784+
}
607785
}
608786
609787
const setYappable = (bool: boolean) => {
610788
market.live2d.canYap = bool
611-
market.live2d.isYapping = false
789+
if (!bool) {
790+
market.live2d.isYapping = false
791+
}
612792
}
613793
614794
watch(() => market.live2d.isYapping, (value) => {
795+
if (!spineCanvas || !spineCanvas.animationState) return
796+
797+
console.log(`[Loader] isYapping changed to: ${value}`)
615798
616-
if (value) {
617-
spineCanvas.animationState.addAnimation(1, YAP_TRACK)
618-
spineCanvas.animationState.setAnimation(1, YAP_TRACK, true)
799+
// Only allow yapping if asset supports it AND user enabled it
800+
if (value && market.live2d.canYap && market.live2d.yapEnabled) {
801+
try {
802+
console.log('[Loader] Setting yap animation')
803+
spineCanvas.animationState.setAnimation(1, YAP_TRACK, true)
804+
} catch (e) {
805+
console.warn('Could not add yap track', e)
806+
}
619807
} else {
620-
spineCanvas.animationState.tracks = [spineCanvas.animationState.tracks[0]]
808+
try {
809+
console.log('[Loader] Clearing yap animation')
810+
spineCanvas.animationState.setEmptyAnimation(1, 0)
811+
} catch (e) {
812+
console.warn('Could not clear yap track', e)
813+
}
621814
}
622815
})
623816

0 commit comments

Comments
 (0)