#!/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 ) data class SimpleChoice( val id: String, val text: String, val nextNodeId: String, val effects: List = emptyList(), val requirements: List = 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("📊 开始内容提取和分析...") // 步骤1:读取并分析CompleteStoryData.kt val storyDataFile = File("app/src/main/java/com/example/gameofmoon/story/CompleteStoryData.kt") if (!storyDataFile.exists()) { println("❌ 找不到CompleteStoryData.kt文件") return } val content = storyDataFile.readText() println("📖 读取了${content.length}个字符的故事内容") // 步骤2:解析节点定义 val nodePattern = Regex(""""([^"]+)"\s+to\s+SimpleStoryNode\s*\([\s\S]*?^\s*\)""", RegexOption.MULTILINE) val nodes = mutableMapOf() nodePattern.findAll(content).forEach { match -> val nodeId = match.groupValues[1] val nodeContent = match.value nodes[nodeId] = nodeContent println("🔍 找到节点:$nodeId") } println("📊 总共找到 ${nodes.size} 个故事节点") // 步骤3:按类型分组节点 val nodeGroups = categorizeNodes(nodes) // 步骤4:生成DSL文件 generateDSLFiles(outputDir, nodeGroups) // 步骤5:生成配置文件 generateConfigFiles(outputDir, nodeGroups) println("✅ 所有文件生成完成") } fun categorizeNodes(nodes: Map): Map> { 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>) { 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 { 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("// 从CompleteStoryData转换的内容:$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>) { 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" }