366 lines
12 KiB
Kotlin
366 lines
12 KiB
Kotlin
#!/usr/bin/env kotlin
|
||
|
||
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||
|
||
import kotlinx.coroutines.*
|
||
import java.io.File
|
||
import java.text.SimpleDateFormat
|
||
import java.util.*
|
||
|
||
/**
|
||
* 独立的迁移测试脚本
|
||
* 用于将CompleteStoryData的内容提取并转换为DSL格式
|
||
*/
|
||
|
||
data class SimpleStoryNode(
|
||
val id: String,
|
||
val title: String,
|
||
val content: String,
|
||
val choices: List<SimpleChoice>
|
||
)
|
||
|
||
data class SimpleChoice(
|
||
val id: String,
|
||
val text: String,
|
||
val nextNodeId: String,
|
||
val effects: List<SimpleEffect> = emptyList(),
|
||
val requirements: List<SimpleRequirement> = emptyList()
|
||
)
|
||
|
||
data class SimpleEffect(
|
||
val type: SimpleEffectType,
|
||
val value: String,
|
||
val description: String = ""
|
||
)
|
||
|
||
data class SimpleRequirement(
|
||
val type: SimpleRequirementType,
|
||
val value: String
|
||
)
|
||
|
||
enum class SimpleEffectType {
|
||
HEALTH_CHANGE, STAMINA_CHANGE, SECRET_UNLOCK, LOCATION_DISCOVER, LOOP_CHANGE, DAY_CHANGE
|
||
}
|
||
|
||
enum class SimpleRequirementType {
|
||
MIN_STAMINA, MIN_HEALTH, HAS_SECRET, VISITED_LOCATION
|
||
}
|
||
|
||
fun main() = runBlocking {
|
||
println("🚀 开始故事内容迁移...")
|
||
|
||
val outputDir = File("migration_output")
|
||
if (outputDir.exists()) {
|
||
outputDir.deleteRecursively()
|
||
}
|
||
outputDir.mkdirs()
|
||
|
||
// 创建子目录
|
||
File(outputDir, "modules").mkdirs()
|
||
File(outputDir, "shared").mkdirs()
|
||
File(outputDir, "config").mkdirs()
|
||
|
||
// 模拟CompleteStoryData的内容提取
|
||
extractAndConvertContent(outputDir)
|
||
|
||
println("✅ 迁移完成!输出目录:${outputDir.absolutePath}")
|
||
}
|
||
|
||
fun extractAndConvertContent(outputDir: File) {
|
||
println("📊 开始内容提取和分析...")
|
||
|
||
// 旧系统 CompleteStoryData.kt 已移除,此处直接跳过读取与解析
|
||
println("ℹ️ 跳过旧系统读取:CompleteStoryData.kt 已移除")
|
||
|
||
val nodes = emptyMap<String, String>()
|
||
println("📊 总共找到 ${nodes.size} 个故事节点")
|
||
|
||
// 步骤3:按类型分组节点
|
||
val nodeGroups = categorizeNodes(nodes)
|
||
|
||
// 步骤4:生成DSL文件
|
||
generateDSLFiles(outputDir, nodeGroups)
|
||
|
||
// 步骤5:生成配置文件
|
||
generateConfigFiles(outputDir, nodeGroups)
|
||
|
||
println("✅ 所有文件生成完成")
|
||
}
|
||
|
||
fun categorizeNodes(nodes: Map<String, String>): Map<String, List<String>> {
|
||
return mapOf(
|
||
"main_chapter_1" to nodes.keys.filter {
|
||
it.contains("awakening") || it.contains("eva_first") || it.contains("medical") ||
|
||
it.contains("exploration") || it.contains("immediate") || it.contains("voice_recognition")
|
||
},
|
||
"main_chapter_2" to nodes.keys.filter {
|
||
it.contains("investigation") || it.contains("revelation") || it.contains("trust") ||
|
||
it.contains("memory") || it.contains("crew_meeting") || it.contains("base_information")
|
||
},
|
||
"main_chapter_3" to nodes.keys.filter {
|
||
it.contains("confrontation") || it.contains("truth") || it.contains("choice") ||
|
||
it.contains("climax") || it.contains("final")
|
||
},
|
||
"emotional_stories" to nodes.keys.filter {
|
||
it.contains("comfort") || it.contains("sharing") || it.contains("identity") ||
|
||
it.contains("inner_strength") || it.contains("gradual_revelation") || it.contains("ethical_discussion")
|
||
},
|
||
"investigation_branch" to nodes.keys.filter {
|
||
it.contains("stealth") || it.contains("eavesdrop") || it.contains("data") ||
|
||
it.contains("evidence") || it.contains("direct_confrontation") || it.contains("system_sabotage")
|
||
},
|
||
"side_stories" to nodes.keys.filter {
|
||
it.contains("garden") || it.contains("photo") || it.contains("crew_analysis") ||
|
||
it.contains("philosophical") || it.contains("memory_reconstruction") || it.contains("private_grief")
|
||
},
|
||
"endings" to nodes.keys.filter {
|
||
it.contains("ending") || it.contains("destruction") || it.contains("eternal_loop") ||
|
||
it.contains("earth_truth") || it.contains("anchor_modification")
|
||
}
|
||
)
|
||
}
|
||
|
||
fun generateDSLFiles(outputDir: File, nodeGroups: Map<String, List<String>>) {
|
||
println("📄 开始生成DSL文件...")
|
||
|
||
for ((groupName, nodeIds) in nodeGroups) {
|
||
if (nodeIds.isEmpty()) continue
|
||
|
||
val dslContent = generateModuleDSL(groupName, nodeIds)
|
||
val outputFile = File(File(outputDir, "modules"), "$groupName.story")
|
||
outputFile.writeText(dslContent)
|
||
|
||
println("📄 生成了 $groupName.story (${nodeIds.size} 个节点)")
|
||
}
|
||
|
||
// 生成共享模块
|
||
generateSharedFiles(outputDir)
|
||
}
|
||
|
||
fun generateModuleDSL(moduleName: String, nodeIds: List<String>): String {
|
||
val dslBuilder = StringBuilder()
|
||
|
||
// 模块头部
|
||
dslBuilder.appendLine("@story_module $moduleName")
|
||
dslBuilder.appendLine("@version 2.0")
|
||
dslBuilder.appendLine("@dependencies [characters, audio_config, anchors]")
|
||
dslBuilder.appendLine("@description \"${getModuleDescription(moduleName)}\"")
|
||
dslBuilder.appendLine()
|
||
|
||
// 音频配置
|
||
dslBuilder.appendLine("@audio")
|
||
dslBuilder.appendLine(" background: ${getModuleAudio(moduleName)}")
|
||
dslBuilder.appendLine(" transition: discovery_chime.mp3")
|
||
dslBuilder.appendLine("@end")
|
||
dslBuilder.appendLine()
|
||
|
||
// 生成节点占位符(实际实现中会解析CompleteStoryData的具体内容)
|
||
for (nodeId in nodeIds) {
|
||
dslBuilder.appendLine("@node $nodeId")
|
||
dslBuilder.appendLine("@title \"${getNodeTitle(nodeId)}\"")
|
||
dslBuilder.appendLine("@audio_bg ${getNodeAudio(nodeId)}")
|
||
dslBuilder.appendLine("@content \"\"")
|
||
dslBuilder.appendLine("// legacy placeholder: $nodeId")
|
||
dslBuilder.appendLine("// 实际内容需要从原始数据中提取")
|
||
dslBuilder.appendLine("\"\"")
|
||
dslBuilder.appendLine()
|
||
|
||
dslBuilder.appendLine("@choices 2")
|
||
dslBuilder.appendLine(" choice_1: \"选择1\" -> next_node_1 [effect: health+5] [audio: button_click.mp3]")
|
||
dslBuilder.appendLine(" choice_2: \"选择2\" -> next_node_2 [effect: trust+2] [audio: notification_beep.mp3]")
|
||
dslBuilder.appendLine("@end")
|
||
dslBuilder.appendLine()
|
||
}
|
||
|
||
return dslBuilder.toString()
|
||
}
|
||
|
||
fun generateSharedFiles(outputDir: File) {
|
||
// 生成角色文件
|
||
val charactersContent = """
|
||
@story_module characters
|
||
@version 2.0
|
||
@description "角色定义模块 - 定义所有游戏角色的属性和特征"
|
||
|
||
@character eva
|
||
name: "伊娃 / EVA"
|
||
voice_style: gentle
|
||
description: "基地AI系统,实际上是莉莉的意识转移,温柔而智慧"
|
||
relationship: "妹妹"
|
||
personality: "关爱、智慧、略带忧郁"
|
||
key_traits: ["protective", "intelligent", "emotional"]
|
||
@end
|
||
|
||
@character alex
|
||
name: "艾利克丝·陈"
|
||
voice_style: determined
|
||
description: "月球基地工程师,坚强而富有同情心的主角"
|
||
relationship: "自己"
|
||
personality: "坚毅、善良、追求真相"
|
||
key_traits: ["brave", "empathetic", "curious"]
|
||
@end
|
||
|
||
@character sara
|
||
name: "萨拉·维特博士"
|
||
voice_style: professional
|
||
description: "基地医生,负责心理健康,内心善良但被迫参与实验"
|
||
relationship: "同事"
|
||
personality: "专业、内疚、渴望救赎"
|
||
key_traits: ["caring", "conflicted", "knowledgeable"]
|
||
@end
|
||
""".trimIndent()
|
||
|
||
File(File(outputDir, "shared"), "characters.story").writeText(charactersContent)
|
||
|
||
// 生成音频配置文件
|
||
val audioContent = """
|
||
@story_module audio_config
|
||
@version 2.0
|
||
@description "音频配置模块 - 定义所有游戏音频资源"
|
||
|
||
@audio
|
||
// ===== 背景音乐 =====
|
||
mysterious: ambient_mystery.mp3
|
||
tension: electronic_tension.mp3
|
||
peaceful: space_silence.mp3
|
||
revelation: orchestral_revelation.mp3
|
||
finale: epic_finale.mp3
|
||
discovery: discovery_chime.mp3
|
||
|
||
// ===== 环境音效 =====
|
||
base_ambient: reactor_hum.mp3
|
||
ventilation: ventilation_soft.mp3
|
||
storm: solar_storm.mp3
|
||
heartbeat: heart_monitor.mp3
|
||
time_warp: time_distortion.mp3
|
||
|
||
// ===== 交互音效 =====
|
||
button_click: button_click.mp3
|
||
notification: notification_beep.mp3
|
||
alert: error_alert.mp3
|
||
@end
|
||
""".trimIndent()
|
||
|
||
File(File(outputDir, "shared"), "audio.story").writeText(audioContent)
|
||
|
||
// 生成锚点配置文件
|
||
val anchorsContent = """
|
||
@story_module anchors
|
||
@version 2.0
|
||
@description "锚点系统 - 定义动态故事导航的智能锚点"
|
||
|
||
@anchor_conditions
|
||
// ===== 关键剧情解锁条件 =====
|
||
eva_reveal_ready: secrets_found >= 3 AND trust_level >= 5
|
||
investigation_unlocked: harrison_recording_found == true
|
||
deep_truth_ready: eva_reveal_ready == true AND investigation_unlocked == true
|
||
perfect_ending_available: secrets_found >= 15 AND health > 50
|
||
|
||
// ===== 结局分支条件 =====
|
||
freedom_ending_ready: anchor_destruction_chosen == true
|
||
loop_ending_ready: eternal_loop_chosen == true
|
||
truth_ending_ready: earth_truth_revealed == true
|
||
|
||
// ===== 情感状态条件 =====
|
||
emotional_stability: health > 70 AND trust_level > 8
|
||
sister_bond_strong: eva_interactions >= 10
|
||
@end
|
||
""".trimIndent()
|
||
|
||
File(File(outputDir, "shared"), "anchors.story").writeText(anchorsContent)
|
||
|
||
println("📄 生成了共享模块文件")
|
||
}
|
||
|
||
fun generateConfigFiles(outputDir: File, nodeGroups: Map<String, List<String>>) {
|
||
val modules = nodeGroups.keys.filter { nodeGroups[it]?.isNotEmpty() == true }
|
||
|
||
val configContent = """
|
||
{
|
||
"version": "2.0",
|
||
"engine": "DSL Story Engine",
|
||
"default_language": "zh",
|
||
"modules": [
|
||
"characters",
|
||
"audio_config",
|
||
"anchors",
|
||
${modules.joinToString(",\n ") { "\"$it\"" }}
|
||
],
|
||
"audio": {
|
||
"enabled": true,
|
||
"default_volume": 0.7,
|
||
"fade_duration": 1000,
|
||
"background_loop": true
|
||
},
|
||
"gameplay": {
|
||
"auto_save": true,
|
||
"choice_timeout": 0,
|
||
"skip_seen_content": false,
|
||
"enable_branching": true
|
||
},
|
||
"features": {
|
||
"conditional_navigation": true,
|
||
"dynamic_anchors": true,
|
||
"memory_management": true,
|
||
"effects_system": true
|
||
},
|
||
"start_node": "first_awakening"
|
||
}
|
||
""".trimIndent()
|
||
|
||
File(File(outputDir, "config"), "config.json").writeText(configContent)
|
||
|
||
val indexContent = """
|
||
{
|
||
"modules": [
|
||
${modules.joinToString(",\n ") { "\"$it\"" }}
|
||
],
|
||
"total_modules": ${modules.size},
|
||
"generated_at": "${SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())}",
|
||
"format_version": "2.0",
|
||
"total_nodes": ${nodeGroups.values.sumOf { it.size }}
|
||
}
|
||
""".trimIndent()
|
||
|
||
File(File(outputDir, "config"), "modules.json").writeText(indexContent)
|
||
|
||
println("📄 生成了配置文件")
|
||
}
|
||
|
||
// 辅助函数
|
||
fun getModuleDescription(moduleName: String): String = when (moduleName) {
|
||
"main_chapter_1" -> "第一章:觉醒 - 主角从昏迷中醒来,开始探索月球基地的秘密"
|
||
"main_chapter_2" -> "第二章:调查 - 深入基地,发现时间锚项目的真相"
|
||
"main_chapter_3" -> "第三章:抉择 - 面对真相,做出最终的选择"
|
||
"emotional_stories" -> "情感故事模块 - 探索角色间的情感联系和内心成长"
|
||
"investigation_branch" -> "调查分支模块 - 深度调查和证据收集的故事线"
|
||
"side_stories" -> "支线故事模块 - 花园、照片记忆等支线剧情"
|
||
"endings" -> "结局模块 - 所有可能的故事结局和终章"
|
||
else -> "故事模块:$moduleName"
|
||
}
|
||
|
||
fun getModuleAudio(moduleName: String): String = when (moduleName) {
|
||
"main_chapter_1" -> "ambient_mystery.mp3"
|
||
"main_chapter_2" -> "electronic_tension.mp3"
|
||
"main_chapter_3" -> "orchestral_revelation.mp3"
|
||
"emotional_stories" -> "space_silence.mp3"
|
||
"investigation_branch" -> "electronic_tension.mp3"
|
||
"side_stories" -> "space_silence.mp3"
|
||
"endings" -> "epic_finale.mp3"
|
||
else -> "ambient_mystery.mp3"
|
||
}
|
||
|
||
fun getNodeTitle(nodeId: String): String {
|
||
return nodeId.split("_").joinToString(" ") {
|
||
it.replaceFirstChar { char -> char.uppercase() }
|
||
}
|
||
}
|
||
|
||
fun getNodeAudio(nodeId: String): String = when {
|
||
nodeId.contains("revelation") || nodeId.contains("truth") -> "orchestral_revelation.mp3"
|
||
nodeId.contains("tension") || nodeId.contains("confrontation") -> "electronic_tension.mp3"
|
||
nodeId.contains("garden") || nodeId.contains("peaceful") -> "space_silence.mp3"
|
||
nodeId.contains("discovery") -> "discovery_chime.mp3"
|
||
else -> "ambient_mystery.mp3"
|
||
}
|