Files
ai_game/client/js/scenes/ChapterScene.js

295 lines
9.1 KiB
JavaScript
Raw Normal View History

/**
* 章节选择场景
*/
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 });
}
}