Files
ai_game/client/js/scenes/ChapterScene.js
2026-03-06 13:16:54 +08:00

295 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 章节选择场景
*/
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 bottomPadding = 50; // 底部留出空间
const contentHeight = this.nodeList.length * (cardHeight + gap) + headerHeight + bottomPadding;
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight);
console.log('[ChapterScene] nodeList长度:', this.nodeList.length, 'contentHeight:', contentHeight, 'screenHeight:', this.screenHeight, 'maxScrollY:', this.maxScrollY);
}
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.4)';
this.roundRect(ctx, this.screenWidth - 8, scrollBarY, 5, scrollBarHeight, 2.5);
ctx.fill();
// 如果还没滚动到底部,显示提示
if (this.scrollY < this.maxScrollY - 10) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('↑ 上滑查看更多 ↑', this.screenWidth / 2, this.screenHeight - 15);
}
}
}
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 });
}
}