diff --git a/engine/types.ts b/engine/types.ts index 40e12bf..1628a57 100644 --- a/engine/types.ts +++ b/engine/types.ts @@ -161,4 +161,6 @@ export interface PlayerTreeNode { locked: boolean lockHint?: string children: PlayerTreeNode[] + isGateway?: boolean + gatewayChapterId?: string } diff --git a/src/components/StoryGallery.vue b/src/components/StoryGallery.vue index 0ca2a57..aca4b35 100644 --- a/src/components/StoryGallery.vue +++ b/src/components/StoryGallery.vue @@ -23,7 +23,11 @@ const emit = defineEmits<{ const showChapterPicker = ref(false) const showDetails = ref(false) -function collectReachable(startId: string): Set { +function isOtherChapterStart(sceneId: string, ownChapterId: string): boolean { + return props.chapters.some(c => c.id !== ownChapterId && c.startScene === sceneId) +} + +function collectReachable(startId: string, chapterId: string): Set { const visited = new Set() const queue = [startId] while (queue.length > 0) { @@ -34,17 +38,22 @@ function collectReachable(startId: string): Set { visited.add(id) if (scene.choices) { for (const c of scene.choices) { - if (c.targetScene && !visited.has(c.targetScene)) queue.push(c.targetScene) + if (c.targetScene && !visited.has(c.targetScene) && !isOtherChapterStart(c.targetScene, chapterId)) + queue.push(c.targetScene) } } - if (scene.nextScene && !visited.has(scene.nextScene)) queue.push(scene.nextScene) + if (scene.nextScene && !visited.has(scene.nextScene) && !isOtherChapterStart(scene.nextScene, chapterId)) + queue.push(scene.nextScene) if (scene.qte) { - if (scene.qte.successScene && !visited.has(scene.qte.successScene)) queue.push(scene.qte.successScene) - if (scene.qte.failScene && !visited.has(scene.qte.failScene)) queue.push(scene.qte.failScene) + if (scene.qte.successScene && !visited.has(scene.qte.successScene) && !isOtherChapterStart(scene.qte.successScene, chapterId)) + queue.push(scene.qte.successScene) + if (scene.qte.failScene && !visited.has(scene.qte.failScene) && !isOtherChapterStart(scene.qte.failScene, chapterId)) + queue.push(scene.qte.failScene) } if (scene.hotspots) { for (const h of scene.hotspots) { - if (h.targetScene && !visited.has(h.targetScene)) queue.push(h.targetScene) + if (h.targetScene && !visited.has(h.targetScene) && !isOtherChapterStart(h.targetScene, chapterId)) + queue.push(h.targetScene) } } } @@ -54,7 +63,7 @@ function collectReachable(startId: string): Set { const chapterReachable = computed(() => { const result: Record> = {} for (const ch of props.chapters) { - result[ch.id] = collectReachable(ch.startScene) + result[ch.id] = collectReachable(ch.startScene, ch.id) } return result }) @@ -129,7 +138,7 @@ function selectChapter(chapterId: string) { showChapterPicker.value = false } -function buildPlayerTree(sceneId: string, depth: number, pathSet: Set): PlayerTreeNode | null { +function buildPlayerTree(sceneId: string, chapterId: string, depth: number, pathSet: Set): PlayerTreeNode | null { if (depth > 10) return null if (pathSet.has(sceneId)) return null const scene = props.scenes[sceneId] @@ -140,35 +149,34 @@ function buildPlayerTree(sceneId: string, depth: number, pathSet: Set): pathSet.add(sceneId) const children: PlayerTreeNode[] = [] if (scene) { - if (scene.choices) { - for (const c of scene.choices) { - if (c.targetScene) { - const child = buildPlayerTree(c.targetScene, depth + 1, pathSet) - if (child) children.push(child) - } + function pushChild(target: string | undefined) { + if (!target) return + if (isOtherChapterStart(target, chapterId)) { + const gatewayCh = props.chapters.find(c => c.startScene === target) + children.push({ + sceneId: '', + label: gatewayCh ? (t(gatewayCh.labelKey || gatewayCh.label)) : target, + visited: false, + locked: !props.unlockedChapterIds.has(gatewayCh?.id ?? ''), + children: [], + isGateway: true, + gatewayChapterId: gatewayCh?.id, + }) + return } - } - if (scene.nextScene) { - const child = buildPlayerTree(scene.nextScene, depth + 1, pathSet) + const child = buildPlayerTree(target, chapterId, depth + 1, pathSet) if (child) children.push(child) } + if (scene.choices) { + for (const c of scene.choices) pushChild(c.targetScene) + } + pushChild(scene.nextScene) if (scene.qte) { - if (scene.qte.successScene) { - const child = buildPlayerTree(scene.qte.successScene, depth + 1, pathSet) - if (child) children.push(child) - } - if (scene.qte.failScene) { - const child = buildPlayerTree(scene.qte.failScene, depth + 1, pathSet) - if (child) children.push(child) - } + pushChild(scene.qte.successScene) + pushChild(scene.qte.failScene) } if (scene.hotspots) { - for (const h of scene.hotspots) { - if (h.targetScene) { - const child = buildPlayerTree(h.targetScene, depth + 1, pathSet) - if (child) children.push(child) - } - } + for (const h of scene.hotspots) pushChild(h.targetScene) } } pathSet.delete(sceneId) @@ -178,7 +186,7 @@ function buildPlayerTree(sceneId: string, depth: number, pathSet: Set): function buildTreeForChapter(chapterId: string): PlayerTreeNode | null { const ch = props.chapters.find(c => c.id === chapterId) if (!ch) return null - return buildPlayerTree(ch.startScene, 0, new Set()) + return buildPlayerTree(ch.startScene, chapterId, 0, new Set()) } function onSelectScene(sceneId: string) { @@ -208,6 +216,7 @@ const tree = computed(() => buildTreeForChapter(currentChapterId.value)) :scenes="scenes" :key="currentChapterId" @select-scene="onSelectScene" + @select-gateway="selectChapter" />
暂无故事数据
diff --git a/src/components/TreeFlow.vue b/src/components/TreeFlow.vue index e4e86c2..7a24b1a 100644 --- a/src/components/TreeFlow.vue +++ b/src/components/TreeFlow.vue @@ -10,6 +10,7 @@ const props = defineProps<{ const emit = defineEmits<{ selectScene: [sceneId: string] + selectGateway: [chapterId: string] }>() interface FlowNode { @@ -19,6 +20,8 @@ interface FlowNode { thumbnail?: string visited: boolean isMystery: boolean + isGateway: boolean + gatewayChapterId?: string locked: boolean lockHint?: string x: number @@ -40,10 +43,29 @@ const containerW = ref(800) const containerH = ref(400) function buildFlow(root: PlayerTreeNode) { - const dagreNodes: { id: string; sceneId: string; thumbnail?: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; locked: boolean; lockHint?: string }[] = [] + const dagreNodes: { id: string; sceneId: string; thumbnail?: string; parent: string | null; label: string; visited: boolean; isMystery: boolean; isGateway: boolean; gatewayChapterId?: string; locked: boolean; lockHint?: string }[] = [] const dagreEdges: { from: string; to: string; visited: boolean }[] = [] function walk(node: PlayerTreeNode, parentId: string | null) { + if (node.isGateway) { + const dagreId = parentId ? `${parentId}/gw_${node.gatewayChapterId}` : `gw_${node.gatewayChapterId}` + dagreNodes.push({ + id: dagreId, + sceneId: '', + parent: parentId, + label: node.label, + visited: false, + isMystery: false, + isGateway: true, + gatewayChapterId: node.gatewayChapterId, + locked: node.locked, + }) + if (parentId) { + dagreEdges.push({ from: parentId, to: dagreId, visited: false }) + } + return + } + if (node.visited) { const dagreId = parentId ? `${parentId}/${node.sceneId}` : node.sceneId dagreNodes.push({ @@ -54,6 +76,7 @@ function buildFlow(root: PlayerTreeNode) { label: node.label, visited: true, isMystery: false, + isGateway: false, locked: node.locked, lockHint: node.lockHint, }) @@ -63,7 +86,7 @@ function buildFlow(root: PlayerTreeNode) { const unvisited: PlayerTreeNode[] = [] for (const child of node.children) { - if (child.visited) { + if (child.visited || child.isGateway) { walk(child, dagreId) } else { unvisited.push(child) @@ -79,6 +102,7 @@ function buildFlow(root: PlayerTreeNode) { label: '? ?', visited: false, isMystery: true, + isGateway: false, locked: true, }) dagreEdges.push({ from: dagreId, to: mysteryId, visited: false }) @@ -120,6 +144,8 @@ function buildFlow(root: PlayerTreeNode) { label: n.label, visited: n.visited, isMystery: n.isMystery, + isGateway: n.isGateway || false, + gatewayChapterId: n.gatewayChapterId, locked: n.locked, lockHint: n.lockHint, x: pos.x - nw / 2, @@ -238,13 +264,13 @@ const svgH = computed(() => containerH.value) v-for="n in nodes" :key="n.id" class="flow-node" - :class="{ visited: n.visited, mystery: n.isMystery, locked: n.locked, clickable: n.visited && n.sceneId }" + :class="{ visited: n.visited, mystery: n.isMystery, locked: n.locked, gateway: n.isGateway, clickable: (n.visited && n.sceneId) || n.isGateway }" :style="{ left: n.x + 'px', top: n.y + 'px', width: n.w + 'px', height: n.h + 'px' }" :title="n.lockHint || ''" - @click="n.visited && n.sceneId && emit('selectScene', n.sceneId)" + @click="n.isGateway ? emit('selectGateway', n.gatewayChapterId!) : n.visited && n.sceneId && emit('selectScene', n.sceneId)" > - {{ n.visited ? '✦' : n.isMystery ? '?' : '⬜' }} + {{ n.visited ? '✦' : n.isMystery ? '?' : n.isGateway ? '►' : '⬜' }} {{ n.label }} @@ -306,6 +332,24 @@ const svgH = computed(() => containerH.value) border-color: rgba(201, 168, 76, 0.5); } +.flow-node.gateway { + background: rgba(201, 168, 76, 0.04); + border: 1px dashed rgba(201, 168, 76, 0.2); +} + +.flow-node.gateway:hover { + background: rgba(201, 168, 76, 0.12); + border-color: rgba(201, 168, 76, 0.4); +} + +.flow-node.gateway .node-icon { + color: #c9a84c; +} + +.flow-node.gateway.locked { + opacity: 0.4; +} + .flow-node.visited { background: rgba(201, 168, 76, 0.12); border: 1px solid rgba(201, 168, 76, 0.3);