/** * 章节选择场景 */ import BaseScene from './BaseScene'; export default class ChapterScene extends BaseScene { constructor(main, params) { super(main, params); this.storyId = params.storyId; this.story = null; this.nodeList = []; this.scrollY = 0; this.maxScrollY = 0; this.isDragging = false; this.lastTouchY = 0; this.hasMoved = false; } init() { this.story = this.main.storyManager.currentStory; if (!this.story || !this.story.nodes) { this.main.sceneManager.switchScene('home'); return; } this.buildNodeList(); this.calculateMaxScroll(); } buildNodeList() { const nodes = this.story.nodes; this.nodeList = []; // 遍历所有节点 Object.keys(nodes).forEach(key => { const node = nodes[key]; this.nodeList.push({ key: key, title: this.getNodeTitle(node, key), isEnding: node.is_ending, endingName: node.ending_name, endingType: node.ending_type, speaker: node.speaker, preview: this.getPreview(node.content) }); }); // 按关键字排序,start在前,ending在后 this.nodeList.sort((a, b) => { if (a.key === 'start') return -1; if (b.key === 'start') return 1; if (a.isEnding && !b.isEnding) return 1; if (!a.isEnding && b.isEnding) return -1; return a.key.localeCompare(b.key); }); } getNodeTitle(node, key) { if (key === 'start') return '故事开始'; if (node.is_ending) return `结局:${node.ending_name || '未知'}`; if (node.speaker && node.speaker !== '旁白') return `${node.speaker}的对话`; return `章节 ${key}`; } getPreview(content) { if (!content) return ''; const text = content.replace(/\n/g, ' ').trim(); return text.length > 40 ? text.substring(0, 40) + '...' : text; } calculateMaxScroll() { const cardHeight = 85; const gap = 12; const headerHeight = 80; const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight; this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20); } update() {} render(ctx) { this.renderBackground(ctx); this.renderHeader(ctx); this.renderNodeList(ctx); } renderBackground(ctx) { const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight); gradient.addColorStop(0, '#0f0c29'); gradient.addColorStop(0.5, '#302b63'); gradient.addColorStop(1, '#24243e'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, this.screenWidth, this.screenHeight); } renderHeader(ctx) { // 顶部遮罩 const headerGradient = ctx.createLinearGradient(0, 0, 0, 80); headerGradient.addColorStop(0, 'rgba(15,12,41,1)'); headerGradient.addColorStop(1, 'rgba(15,12,41,0)'); ctx.fillStyle = headerGradient; ctx.fillRect(0, 0, this.screenWidth, 80); // 返回按钮 ctx.fillStyle = '#ffffff'; ctx.font = '16px sans-serif'; ctx.textAlign = 'left'; ctx.fillText('‹ 返回', 15, 40); // 标题 ctx.textAlign = 'center'; ctx.font = 'bold 17px sans-serif'; ctx.fillText('选择章节', this.screenWidth / 2, 40); // 故事名 ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = '12px sans-serif'; const title = this.story?.title || ''; ctx.fillText(title.length > 15 ? title.substring(0, 15) + '...' : title, this.screenWidth / 2, 58); } renderNodeList(ctx) { const padding = 15; const cardHeight = 85; const cardMargin = 12; const startY = 80; ctx.save(); ctx.beginPath(); ctx.rect(0, startY, this.screenWidth, this.screenHeight - startY); ctx.clip(); this.nodeList.forEach((node, index) => { const y = startY + index * (cardHeight + cardMargin) - this.scrollY; if (y + cardHeight < startY || y > this.screenHeight) return; // 卡片背景 if (node.isEnding) { const endingGradient = ctx.createLinearGradient(padding, y, this.screenWidth - padding, y); const colors = this.getEndingColors(node.endingType); endingGradient.addColorStop(0, colors[0]); endingGradient.addColorStop(1, colors[1]); ctx.fillStyle = endingGradient; } else { ctx.fillStyle = 'rgba(255,255,255,0.08)'; } this.roundRect(ctx, padding, y, this.screenWidth - padding * 2, cardHeight, 12); ctx.fill(); // 节点标题 ctx.fillStyle = node.isEnding ? '#ffffff' : '#ffffff'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'left'; ctx.fillText(node.title, padding + 15, y + 28); // 节点预览 ctx.fillStyle = node.isEnding ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.5)'; ctx.font = '12px sans-serif'; const preview = this.truncateText(ctx, node.preview, this.screenWidth - padding * 2 - 30); ctx.fillText(preview, padding + 15, y + 52); // 节点标识 if (node.key === 'start') { this.renderTag(ctx, this.screenWidth - padding - 60, y + 18, '起点', '#4ecca3'); } else if (node.isEnding) { this.renderTag(ctx, this.screenWidth - padding - 60, y + 18, '结局', '#ffd700'); } }); ctx.restore(); // 滚动条 if (this.maxScrollY > 0) { const scrollBarHeight = 50; const scrollBarY = startY + (this.scrollY / this.maxScrollY) * (this.screenHeight - startY - scrollBarHeight - 20); ctx.fillStyle = 'rgba(255,255,255,0.2)'; this.roundRect(ctx, this.screenWidth - 5, scrollBarY, 3, scrollBarHeight, 1.5); ctx.fill(); } } getEndingColors(type) { switch (type) { case 'good': return ['rgba(100,200,100,0.3)', 'rgba(50,150,50,0.2)']; case 'bad': return ['rgba(200,100,100,0.3)', 'rgba(150,50,50,0.2)']; case 'hidden': return ['rgba(255,215,0,0.3)', 'rgba(200,150,0,0.2)']; default: return ['rgba(150,150,255,0.3)', 'rgba(100,100,200,0.2)']; } } renderTag(ctx, x, y, text, color) { ctx.font = '10px sans-serif'; const width = ctx.measureText(text).width + 12; ctx.fillStyle = color + '40'; this.roundRect(ctx, x, y, width, 18, 9); ctx.fill(); ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.fillText(text, x + width / 2, y + 13); ctx.textAlign = 'left'; } truncateText(ctx, text, maxWidth) { if (!text) return ''; if (ctx.measureText(text).width <= maxWidth) return text; let truncated = text; while (truncated.length > 0 && ctx.measureText(truncated + '...').width > maxWidth) { truncated = truncated.slice(0, -1); } return truncated + '...'; } roundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } onTouchStart(e) { const touch = e.touches[0]; this.lastTouchY = touch.clientY; this.touchStartY = touch.clientY; this.touchStartX = touch.clientX; this.isDragging = true; this.hasMoved = false; } onTouchMove(e) { if (!this.isDragging) return; const touch = e.touches[0]; const deltaY = this.lastTouchY - touch.clientY; if (Math.abs(deltaY) > 3) { this.hasMoved = true; } this.scrollY += deltaY; this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY)); this.lastTouchY = touch.clientY; } onTouchEnd(e) { this.isDragging = false; const touch = e.changedTouches[0]; const x = touch.clientX; const y = touch.clientY; if (this.hasMoved) return; // 返回按钮 if (y < 60 && x < 80) { this.main.sceneManager.switchScene('ending', { storyId: this.storyId, ending: this.main.storyManager.getEndingInfo() }); return; } // 点击节点 const padding = 15; const cardHeight = 85; const cardMargin = 12; const startY = 80; for (let i = 0; i < this.nodeList.length; i++) { const cardY = startY + i * (cardHeight + cardMargin) - this.scrollY; if (y >= cardY && y <= cardY + cardHeight && x >= padding && x <= this.screenWidth - padding) { this.selectNode(this.nodeList[i].key); return; } } } selectNode(nodeKey) { this.main.storyManager.currentNodeKey = nodeKey; this.main.sceneManager.switchScene('story', { storyId: this.storyId }); } }