diff --git a/RVM_Beta1.0.zip b/RVM_Beta1.0.zip new file mode 100644 index 0000000..521bdb4 Binary files /dev/null and b/RVM_Beta1.0.zip differ diff --git a/RVM_Beta1.0/RVM_Beta1.0/RVM Rimworld Visual Mod Maker Installation and Usage Guide - Beta 1.0.pdf b/RVM_Beta1.0/RVM_Beta1.0/RVM Rimworld Visual Mod Maker Installation and Usage Guide - Beta 1.0.pdf new file mode 100644 index 0000000..abc7d59 Binary files /dev/null and b/RVM_Beta1.0/RVM_Beta1.0/RVM Rimworld Visual Mod Maker Installation and Usage Guide - Beta 1.0.pdf differ diff --git a/RVM_Beta1.0/RVM_Beta1.0/RVM安装与使用指南Beta1.0.pdf b/RVM_Beta1.0/RVM_Beta1.0/RVM安装与使用指南Beta1.0.pdf new file mode 100644 index 0000000..ed8efb4 Binary files /dev/null and b/RVM_Beta1.0/RVM_Beta1.0/RVM安装与使用指南Beta1.0.pdf differ diff --git a/RVM_Beta1.0/RVM_Beta1.0/index.html b/RVM_Beta1.0/RVM_Beta1.0/index.html new file mode 100644 index 0000000..435c181 --- /dev/null +++ b/RVM_Beta1.0/RVM_Beta1.0/index.html @@ -0,0 +1,12 @@ + + + + + + RimWorld Visual Mod Maker v50 + + +
+ + + diff --git a/RVM_Beta1.0/RVM_Beta1.0/package-lock.json b/RVM_Beta1.0/RVM_Beta1.0/package-lock.json new file mode 100644 index 0000000..a2a59ba --- /dev/null +++ b/RVM_Beta1.0/RVM_Beta1.0/package-lock.json @@ -0,0 +1,1242 @@ +{ + "name": "rimworld-visual-mod-maker-v50", + "version": "0.50.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rimworld-visual-mod-maker-v50", + "version": "0.50.0", + "dependencies": { + "jszip": "3.10.1", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/react": "18.2.66", + "@types/react-dom": "18.2.22", + "typescript": "5.4.5", + "vite": "5.4.21" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.2.66", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", + "integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/RVM_Beta1.0/RVM_Beta1.0/package.json b/RVM_Beta1.0/RVM_Beta1.0/package.json new file mode 100644 index 0000000..6e233b1 --- /dev/null +++ b/RVM_Beta1.0/RVM_Beta1.0/package.json @@ -0,0 +1,22 @@ +{ + "name": "rimworld-visual-mod-maker-v50", + "version": "0.50.0", + "private": true, + "type": "module", + "scripts": { + "web": "vite --host 127.0.0.1", + "dev": "vite --host 127.0.0.1", + "build": "tsc && vite build" + }, + "dependencies": { + "jszip": "3.10.1", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/react": "18.2.66", + "@types/react-dom": "18.2.22", + "typescript": "5.4.5", + "vite": "5.4.21" + } +} diff --git a/RVM_Beta1.0/RVM_Beta1.0/src/main.tsx b/RVM_Beta1.0/RVM_Beta1.0/src/main.tsx new file mode 100644 index 0000000..e42fc34 --- /dev/null +++ b/RVM_Beta1.0/RVM_Beta1.0/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./ui/App"; +import "./ui/styles.css"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/RVM_Beta1.0/RVM_Beta1.0/src/ui/App.tsx b/RVM_Beta1.0/RVM_Beta1.0/src/ui/App.tsx new file mode 100644 index 0000000..19c5f22 --- /dev/null +++ b/RVM_Beta1.0/RVM_Beta1.0/src/ui/App.tsx @@ -0,0 +1,1936 @@ +import React, { ChangeEvent, DragEvent, useEffect, useMemo, useRef, useState } from "react"; +import JSZip from "jszip"; + +type Lang = "en" | "zh"; +type Direction = "front" | "side" | "back" | "single"; +type ItemKind = "hair" | "food" | "apparel" | "weapon" | "building" | "generic"; +type WeaponMode = "melee" | "rangedAuto" | "rangedSingle" | "beamLaser"; +type WeaponSoundPreset = "AssaultRifle" | "BoltActionRifle" | "SniperRifle" | "Revolver" | "Autopistol" | "HeavySMG" | "LMG" | "ChargeRifle" | "Minigun" | "BeamSilent" | "BeamFlamethrower"; +type BeamVisualPreset = "safeNone" | "anomalyIncinerator" | "custom"; +type TextureMode = "shared" | "gendered" | "bodyTypes"; +type HeadTextureMode = "shared" | "gendered"; +type TechLevel = "Neolithic" | "Medieval" | "Industrial" | "Spacer" | "Ultra" | "Archotech"; +type ResearchTreeOwnership = "vanilla" | "independent"; + +type Asset = { + id: string; + label: string; + fileName: string; + mimeType: string; + dataUrl: string; +}; + +type LogEntry = { + id: string; + timestamp: string; + scope: string; + action: string; + detail: string; +}; + +type Dependency = { + packageId: string; + displayName: string; + required: boolean; + steamWorkshopUrl?: string; + downloadUrl?: string; +}; + +type ModInfo = { + name: string; + packageId: string; + author: string; + description: string; + supportedVersions: string[]; + supportedVersionsCsv?: string; +}; + +type RaceConfig = { + enabled: boolean; + defName: string; + label: string; + description: string; + textureMode: TextureMode; + headTextureMode: HeadTextureMode; + healthScale: number; + moveSpeed: number; + meleeDamage: number; + bodySize: number; + lifeExpectancy: number; + genes: string[]; + xenotypeDefName: string; + xenotypeLabel: string; + xenotypeDescription: string; + preserveTextureColors: boolean; +}; + +type FactionConfig = { + enabled: boolean; + defName: string; + label: string; + fixedName: string; + description: string; + culture: string; + categoryTag: "Outlander" | "Tribal" | "Pirate"; + techLevel: TechLevel; + requiredCountAtGameStart: number; + maxConfigurableAtWorldCreation: number; +}; + +type AddItem = { + id: string; + kind: ItemKind; + defName: string; + label: string; + description: string; + marketValue: number; + mass: number; + flammability: number; + deteriorationRate: number; + beauty: number; + workToMake: number; + steelCost: number; + componentCost: number; + stackLimit: number; + weaponMode: WeaponMode; + damage: number; + cooldown: number; + meleeArmorPenetration: number; + range: number; + warmupTime: number; + burstShotCount: number; + ticksBetweenBurstShots: number; + armorPenetration: number; + projectileSpeed: number; + accuracyTouch: number; + accuracyShort: number; + accuracyMedium: number; + accuracyLong: number; + soundPreset: WeaponSoundPreset; + damageDef: string; + projectileGraphicPath: string; + projectileExplosionRadius: number; + projectileStoppingPower: number; + projectileChanceToStartFire: number; + beamWidth: number; + beamFullWidthRange: number; + beamVisualPreset?: BeamVisualPreset; + beamGroundFleckDef?: string; + beamLineFleckDef?: string; + beamEndEffecterDef?: string; + customBeamSoundDef?: string; + weaponTagsCsv: string; + tradeTagsCsv: string; + thingCategoriesCsv: string; + recipeUsersCsv: string; + madeFromStuff: boolean; + stuffCategoriesCsv: string; + costStuffCount: number; + buildingSizeX: number; + buildingSizeY: number; + buildingPassability: "Standable" | "PassThroughOnly" | "Impassable"; + buildingFillPercent: number; + buildingDesignationCategory: string; + buildingMaxHitPoints: number; + researchPrerequisite: string; +}; + +type ResearchProject = { + id: string; + defName: string; + label: string; + description: string; + baseCost: number; + techLevel: TechLevel; + prerequisites: string[]; + prerequisitesCsv?: string; + techTreeOwnership?: ResearchTreeOwnership; + researchTabDefName?: string; + researchViewX?: number; + researchViewY?: number; +}; + +type StorytellerConfig = { + enabled: boolean; + defName: string; + label: string; + description: string; + baseProfile: "Cassandra" | "Phoebe" | "Randy"; + listOrder: number; +}; + +type PlayerFactionDef = "PlayerColony" | "PlayerTribe"; +type StartingPawnRaceMode = "stableSelectedOnly" | "experimentalCandidatePool"; + +type ScenarioConfig = { + enabled: boolean; + defName: string; + label: string; + summary: string; + description: string; + playerFactionDef: PlayerFactionDef; + forceCustomRaceStartingPawns: boolean; + startingPawnRaceMode: StartingPawnRaceMode; + startingPawnCount: number; + chooseFromPawnCount: number; + startWithSilver: number; + startWithPackagedMeals: number; + startWithMedicine: number; + startWithComponents: number; + startWithSteel: number; +}; + +type Project = { + schemaVersion: string; + language: Lang; + mod: ModInfo; + dependencies: Dependency[]; + race: RaceConfig; + faction: FactionConfig; + items: AddItem[]; + research: ResearchProject[]; + storyteller: StorytellerConfig; + scenario: ScenarioConfig; + assets: Record; + logs: LogEntry[]; +}; + +const STORAGE_KEY = "rimworldVisualModMaker.v50.project"; +const BODY_TYPES = ["Male", "Female", "Thin", "Hulk", "Fat"]; + +const WEAPON_SOUND_PRESETS: Record = { + AssaultRifle: { label: "Assault rifle", soundCast: "Shot_AssaultRifle", soundCastTail: "GunTail_Medium" }, + BoltActionRifle: { label: "Bolt-action rifle", soundCast: "Shot_BoltActionRifle", soundCastTail: "GunTail_Medium" }, + SniperRifle: { label: "Sniper rifle", soundCast: "Shot_SniperRifle", soundCastTail: "GunTail_Heavy" }, + Revolver: { label: "Revolver", soundCast: "Shot_Revolver", soundCastTail: "GunTail_Light" }, + Autopistol: { label: "Autopistol", soundCast: "Shot_Autopistol", soundCastTail: "GunTail_Light" }, + HeavySMG: { label: "Heavy SMG", soundCast: "Shot_HeavySMG", soundCastTail: "GunTail_Medium" }, + LMG: { label: "LMG", soundCast: "Shot_LMG", soundCastTail: "GunTail_Medium" }, + ChargeRifle: { label: "Charge rifle", soundCast: "Shot_ChargeRifle", soundCastTail: "GunTail_Heavy" }, + Minigun: { label: "Minigun", soundCast: "Shot_Minigun", soundCastTail: "GunTail_Heavy" }, + BeamSilent: { label: "Beam weapon: silent safe default", beamOnly: true }, + BeamFlamethrower: { label: "Beam weapon: sustained flamethrower sound", beamSoundCast: "Flamethrower_Firing", soundCastTail: "GunTail_Medium", beamOnly: true } +}; +const HAR_DEP: Dependency = { + packageId: "erdelf.HumanoidAlienRaces", + displayName: "Humanoid Alien Races", + required: true, + steamWorkshopUrl: "steam://url/CommunityFilePage/839005762" +}; + +const GENE_CATEGORIES: Array<{ title: string; genes: Array<{ defName: string; label: string; note: string }> }> = [ + { + title: "Recommended / Common", + genes: [ + { defName: "MeleeDamage_Strong", label: "Strong melee damage", note: "Higher melee damage." }, + { defName: "MeleeDamage_VeryStrong", label: "Very strong melee damage", note: "Much higher melee damage." }, + { defName: "MeleeDamage_Weak", label: "Weak melee damage", note: "Lower melee damage." }, + { defName: "Robust", label: "Robust", note: "Reduced incoming damage." }, + { defName: "Delicate", label: "Delicate", note: "Increased incoming damage." }, + { defName: "MoveSpeed_Quick", label: "Fast runner", note: "Higher movement speed." }, + { defName: "MoveSpeed_VeryQuick", label: "Very fast runner", note: "Much higher movement speed." }, + { defName: "MoveSpeed_Slow", label: "Slow runner", note: "Lower movement speed." }, + { defName: "MoveSpeed_VerySlow", label: "Very slow runner", note: "Much lower movement speed." }, + { defName: "Immunity_Strong", label: "Strong immunity", note: "Faster immunity gain." }, + { defName: "Immunity_SuperStrong", label: "Super immunity", note: "Much faster immunity gain." }, + { defName: "Immunity_Weak", label: "Weak immunity", note: "Slower immunity gain." }, + { defName: "Ageless", label: "Ageless", note: "Biological aging is greatly reduced." } + ] + }, + { + title: "Psy / Mind", + genes: [ + { defName: "PsychicAbility_Dull", label: "Psychically dull", note: "Reduced psychic sensitivity." }, + { defName: "PsychicAbility_Sensitive", label: "Psychically sensitive", note: "Increased psychic sensitivity." }, + { defName: "PsychicAbility_Hypersensitive", label: "Psychically hypersensitive", note: "Greatly increased psychic sensitivity." }, + { defName: "PsychicBonding", label: "Psychic bonding", note: "Psychic bonding related gene." }, + { defName: "ViolenceDisabled", label: "Violence disabled", note: "Cannot perform violent work." } + ] + }, + { + title: "Body / Metabolism", + genes: [ + { defName: "Nearsighted", label: "Nearsighted", note: "Reduced shooting accuracy." }, + { defName: "StrongStomach", label: "Strong stomach", note: "Can better tolerate unsafe food." }, + { defName: "NeverSleep", label: "Never sleep", note: "No sleep need." }, + { defName: "Sleepy", label: "Sleepy", note: "Higher sleep need." }, + { defName: "LowSleep", label: "Low sleep", note: "Lower sleep need." }, + { defName: "VeryLowSleep", label: "Very low sleep", note: "Much lower sleep need." }, + { defName: "FastWoundHealing", label: "Fast wound healing", note: "Wounds heal faster." }, + { defName: "SlowWoundHealing", label: "Slow wound healing", note: "Wounds heal slower." } + ] + }, + { + title: "Appearance / Hair / Skin", + genes: [ + { defName: "Hair_LongOnly", label: "Long hair only", note: "Restricts generated hair to long styles." }, + { defName: "Hair_Bald", label: "Bald", note: "No hair." }, + { defName: "Beard_Always", label: "Always beard", note: "Forces beard generation where supported." }, + { defName: "Skin_Light", label: "Light skin", note: "Light skin tone gene." }, + { defName: "Skin_Dark", label: "Dark skin", note: "Dark skin tone gene." }, + { defName: "HairColor_Blond", label: "Blond hair", note: "Hair color gene." }, + { defName: "HairColor_Dark", label: "Dark hair", note: "Hair color gene." }, + { defName: "HairColor_Red", label: "Red hair", note: "Hair color gene." } + ] + }, + { + title: "Special / Archite", + genes: [ + { defName: "Deathless", label: "Deathless", note: "Archite deathless style gene." }, + { defName: "Hemogenic", label: "Hemogenic", note: "Hemogen system related gene." }, + { defName: "FireResistant", label: "Fire resistant", note: "Better resistance to fire." }, + { defName: "ToxicResistance", label: "Toxic resistance", note: "Better resistance to toxins." }, + { defName: "ToxicWeakness", label: "Toxic weakness", note: "Worse toxin resistance." } + ] + } +]; + +const GENE_ALIAS: Record = { + StrongMeleeDamage: "MeleeDamage_Strong", + FastRunner: "MoveSpeed_Quick", + SuperImmune: "Immunity_SuperStrong", + SlowAging: "Ageless", + PsychicallyDull: "PsychicAbility_Dull" +}; + +const tDict: Record = { + "RimWorld Visual Mod Maker": "RimWorld 可视化 Mod 制作器", + "Mod Basic Info": "Mod 基础信息", + "Dependencies": "依赖项", + "Custom Race": "自定义种族", + "Genes": "基因", + "Faction": "派系", + "Add Item": "添加物品", + "Tech Tree": "科技树", + "Storyteller": "故事叙述者", + "Scenario": "剧本", + "Assets": "素材", + "Export": "导出", + "Interface language/UI Language": "界面语言/UI Language", + "English": "英文", + "Chinese": "中文", + "Mod name": "Mod 名称", + "Package ID": "Package ID", + "Author": "作者", + "Description": "描述", + "Supported versions, English comma only": "支持版本,仅使用英文逗号 , 分隔", + "Add dependency": "添加依赖", + "Required": "必需", + "Display name": "显示名称", + "Steam Workshop URL": "Steam 创意工坊 URL", + "Download URL": "下载 URL", + "Enable Custom Race / HAR RaceDef": "启用自定义种族 / HAR RaceDef", + "Enable FactionDef": "启用派系 FactionDef", + "Enable custom StorytellerDef": "启用自定义 StorytellerDef", + "Enable custom ScenarioDef": "启用自定义 ScenarioDef", + "Player faction": "玩家开始派系", + "New arrivals / Colony": "新移民 / 殖民地", + "New tribe / Tribal": "新部落 / 部落", + "Force starting pawns to use custom race": "强制开局角色使用自定义种族", + "Strict custom race start": "严格自定义种族开局", + "When Custom Race is enabled, this uses Biotech's ConfigurePawnsXenotypes scenPart to generate starting pawns from the custom PawnKind instead of vanilla Human pawns.": "启用自定义种族时,这会使用 Biotech 的 ConfigurePawnsXenotypes 剧本部件,让开局角色从自定义 PawnKind 生成,而不是原版 Human。", + "Strict mode uses startingPawnCount as the actual pawnChoiceCount when exporting. This prevents reserve candidates and the Random button from falling back to vanilla Human graphics; chooseFromPawnCount is ignored while this option is enabled.": "严格模式导出时会用 startingPawnCount 作为实际 pawnChoiceCount。这样可以避免备选角色和随机按钮回退到原版 Human 贴图;启用此选项时 chooseFromPawnCount 会被忽略。", + "chooseFromPawnCount is ignored while this option is enabled.": "启用此选项时 chooseFromPawnCount 会被忽略。", + "Unique identifier defName": "唯一标识符 defName", + "Label": "显示名 label", + "Health scale": "生命倍率", + "Move speed": "移动速度", + "Melee damage": "近战伤害", + "Body size": "身体大小", + "Life expectancy": "寿命", + "Texture mode": "贴图模式", + "Shared one set": "通用一套", + "Gendered male/female": "按性别区分", + "Body types": "按体型区分", + "Head texture mode": "头部贴图模式", + "Preserve original texture colors": "保留原始贴图颜色", + "Disable pawn skin tinting and use white skinColor in HAR graphicPaths.": "关闭 RimWorld/HAR 对身体贴图的肤色染色,并在 HAR graphicPaths 中使用白色 skinColor。", + "Starting pawn race mode": "开局人物种族模式", + "Stable custom race start": "稳定自定义种族开局", + "Experimental candidate pool": "实验候选池", + "Custom race start note": "RimWorld/HAR 纯 XML 无法稳定同时做到备选池和随机按钮都使用 HAR 自定义 RaceDef;稳定模式会让已选开局角色正确使用自定义种族,候选池可能显示 Human。", + "Experimental candidate pool note": "实验模式会尝试把更多候选人绑定到自定义 Xenotype/PawnKind,但部分游戏版本可能把候选数量当作已选数量。", + "Drop PNG here or click": "拖入 PNG 或点击上传", + "Missing PNG": "缺少 PNG", + "Uploaded": "已上传", + "Add GeneDef": "添加 GeneDef", + "Delete item": "删除物品", + "Melee settings": "近战设置", + "Ranged settings": "远程设置", + "Recommended / Common": "推荐 / 常用", + "Psy / Mind": "灵能 / 心智", + "Body / Metabolism": "身体 / 代谢", + "Appearance / Hair / Skin": "外观 / 头发 / 肤色", + "Special / Archite": "特殊 / Archite", + "Add typed GeneDef": "添加输入的 GeneDef", + "Search GeneDef": "搜索 GeneDef", + "Faction defName": "派系 defName", + "Fixed world name": "地图固定名称", + "Culture description": "文化描述", + "Category": "派系类型", + "Tech level": "科技水平", + "Required count at game start": "开局必定生成数量", + "Max configurable at world creation": "世界创建可添加上限", + "New item": "新物品", + "Item type": "物品类型", + "Weapon mode": "武器模式", + "Automatic ranged weapon": "自动远程武器", + "Single-shot ranged weapon": "单发远程武器", + "Laser / beam weapon": "激光 / 光束武器", + "Beam settings": "光束设置", + "Beam width": "光束宽度", + "Beam full-width range": "光束最大宽度距离", + "Beam weapons use Verb_ShootBeam and Beam-style damage; projectile texture and projectile speed are ignored.": "激光武器使用 Verb_ShootBeam 和 Beam 类伤害;弹丸贴图和弹速会被忽略。", + "Vanilla sound preset": "原版声音模板", + "Melee weapon": "近战武器", + "Damage": "伤害", + "Cooldown": "冷却", + "Range": "射程", + "Burst shots": "连发次数", + "Ticks between burst shots": "连发间隔 ticks", + "Research prerequisite": "前置科技", + "New research": "新科技", + "Base cost": "研究成本", + "Prerequisites, English comma only": "前置科技,仅使用英文逗号 , 分隔", + "Export project JSON": "导出项目 JSON", + "Import project JSON / generated ZIP": "导入项目 JSON / 已生成 ZIP", + "Export Mod ZIP": "导出 Mod ZIP", + "Validation": "校验", + "No blocking errors": "没有阻断错误", + "Save status": "保存状态", + "Autosaved locally": "已自动本地保存", + "Clear local project": "清空本地项目", + "Remote weapons now default to automatic fire; you can still switch to single-shot if needed.": "远程武器现在默认自动射击,也可以手动切换成单发。", + "Custom Race automatically adds Humanoid Alien Races as a required dependency only when enabled.": "只有启用自定义种族时,才会自动添加 Humanoid Alien Races 依赖。", + "Progress is autosaved to localStorage; export a project JSON before moving computers or clearing browser data.": "进度会自动保存到 localStorage;换电脑或清浏览器数据前请导出项目 JSON。", + "Projects can be reloaded from exported project JSON in the editor source/ folder.": "可从编辑器 source/ 文件夹中的项目 JSON 重新导入继续编辑。", + "Tech tree support creates ResearchProjectDef and can link items through researchPrerequisite.": "科技树会生成 ResearchProjectDef,并可通过 researchPrerequisite 绑定物品。", + "Advanced item editor": "高级物品编辑器", + "Base item stats": "物品基础数值", + "Work to make": "制作工时", + "Steel cost": "钢铁成本", + "Component cost": "零部件成本", + "Mass": "重量", + "Flammability": "易燃性", + "Deterioration rate": "老化速率", + "Beauty": "美观度", + "Melee armor penetration": "近战护甲穿透", + "Ranged armor penetration": "远程护甲穿透", + "Warmup time": "瞄准时间", + "Projectile speed": "弹速", + "Accuracy touch": "贴身精度", + "Accuracy short": "近距离精度", + "Accuracy medium": "中距离精度", + "Accuracy long": "远距离精度", + "This editor now writes an independent projectile for ranged weapons. The sound preset only changes soundCast/soundCastTail and never overwrites your weapon stats.": "当前编辑器会为远程武器生成独立弹丸。声音模板只改变 soundCast/soundCastTail,不会覆盖你的武器数值。", + "Damage type damageDef": "伤害类型 damageDef", + "Projectile texture path": "弹丸贴图路径", + "Stopping power": "停止力", + "Explosion radius": "爆炸半径", + "Fire chance": "点燃概率", + "Fire chance is currently UI-only for XML safety; use Flame damageDef with Explosion radius for fire-style explosions.": "为避免 XML 红字,点燃概率当前仅保留在项目数据中;可使用 Flame damageDef + 爆炸半径制作火焰爆炸。", + "Crafting / Tags": "制作 / 标签", + "Recipe users, English comma only": "制作工作台,仅使用英文逗号 , 分隔", + "Weapon tags, English comma only": "武器标签,仅使用英文逗号 , 分隔", + "Trade tags, English comma only": "交易标签,仅使用英文逗号 , 分隔", + "Thing categories, English comma only": "物品分类,仅使用英文逗号 , 分隔", + "Made from stuff": "使用材料系统 madeFromStuff", + "Stuff categories, English comma only": "材料类别,仅使用英文逗号 , 分隔", + "Stuff cost count": "材料消耗数量", + "Expand all": "全部展开", + "Collapse all": "全部收起", + "Click header to expand or collapse this node.": "点击标题可展开或收起这个节点。", + "Collapsed item": "已折叠物品", + "Collapsed research": "已折叠科技", + "Tech tree ownership": "科技树归属", + "Merge into vanilla tech tree": "合并至原版科技树", + "Create independent tech tree": "新建独立科技树", + "ResearchTabDef for independent tree": "独立科技树 ResearchTabDef", + "Research view X": "科技树坐标 X", + "Research view Y": "科技树坐标 Y", + "Independent tree note": "选择独立科技树时,导出会生成 ResearchTabDef 并让该科技指向它。", + "Vanilla merge note": "选择合并至原版科技树时,该科技会写入原版 Main 科技页。", + "Delete research": "删除科技", + "No items yet. Click New item to add one.": "还没有物品。点击新物品添加。", + "Chinese punctuation in comma fields will be converted to English commas before export.": "逗号分隔字段中的中文标点会在导出前转换为英文逗号。", + "No research projects yet. Click New research to add one.": "还没有科技项目。点击新科技添加。", + "Building": "可建造物", + "Building settings": "可建造物设置", + "Work to build": "建造工时", + "Size X": "尺寸 X", + "Size Y": "尺寸 Y", + "Designation category": "建造分类", + "Passability": "通行性", + "Fill percent": "占用比例", + "Max hit points": "最大耐久", + "Buildable items export as safe standalone ThingDef and appear in Architect tabs.": "可建造物会导出为安全独立 ThingDef,并显示在建造菜单分类中。", + "Beam sound preset": "光束声音模板", + "Beam custom SoundDef optional": "自定义光束 SoundDef(可选)", + "Beam sound note": "Verb_ShootBeam 需要可持续播放的 sustainer 声音;普通枪声会报错。默认不写 soundCastBeam,避免红字。", +}; + +function tr(language: Lang, text: string): string { + return language === "zh" ? tDict[text] ?? text : text; +} + +function defaultProject(): Project { + return { + schemaVersion: "rimworld-visual-mod-maker.project.v50", + language: "en", + mod: { + name: "TestModName", + packageId: "test.testmodname", + author: "TestAuthor", + description: "TestDescription", + supportedVersions: ["1.6"], + supportedVersionsCsv: "1.6" + }, + dependencies: [], + race: { + enabled: false, + defName: "TestUniqueIdentifierDefName", + label: "test race", + description: "TestRaceDescription", + textureMode: "shared", + headTextureMode: "shared", + healthScale: 1, + moveSpeed: 4.6, + meleeDamage: 8, + bodySize: 1, + lifeExpectancy: 80, + genes: [], + xenotypeDefName: "TestXenotypeDefName", + xenotypeLabel: "test xenotype", + xenotypeDescription: "TestXenotypeDescription", + preserveTextureColors: false + }, + faction: { + enabled: false, + defName: "TestFactionDefName", + label: "test faction", + fixedName: "TestFactionFixedName", + description: "TestFactionDescription", + culture: "TestFactionCultureDescription", + categoryTag: "Outlander", + techLevel: "Industrial", + requiredCountAtGameStart: 1, + maxConfigurableAtWorldCreation: 20 + }, + items: [], + research: [], + storyteller: { + enabled: false, + defName: "TestStorytellerDefName", + label: "TestStorytellerLabel", + description: "TestStorytellerDescription", + baseProfile: "Cassandra", + listOrder: 95 + }, + scenario: { + enabled: false, + defName: "TestScenarioDefName", + label: "TestScenarioLabel", + summary: "TestScenarioSummary", + description: "TestScenarioDescription", + playerFactionDef: "PlayerColony", + forceCustomRaceStartingPawns: true, + startingPawnRaceMode: "stableSelectedOnly", + startingPawnCount: 3, + chooseFromPawnCount: 8, + startWithSilver: 800, + startWithPackagedMeals: 40, + startWithMedicine: 30, + startWithComponents: 30, + startWithSteel: 450 + }, + assets: {}, + logs: [] + }; +} + +function hydrateProject(raw: Partial): Project { + const base = defaultProject(); + const next: Project = { + ...base, + ...raw, + mod: { ...base.mod, ...(raw.mod ?? {}) }, + race: { ...base.race, ...(raw.race ?? {}) }, + faction: { ...base.faction, ...(raw.faction ?? {}) }, + storyteller: { ...base.storyteller, ...(raw.storyteller ?? {}) }, + scenario: { ...base.scenario, ...(raw.scenario ?? {}) }, + dependencies: raw.dependencies ?? base.dependencies, + items: raw.items ?? base.items, + research: raw.research ?? base.research, + assets: raw.assets ?? base.assets, + logs: raw.logs ?? [] + }; + next.schemaVersion = "rimworld-visual-mod-maker.project.v50"; + next.language = next.language || "en"; + next.items = (next.items ?? []).map((item) => ({ + ...item, + weaponMode: (item.weaponMode ?? "rangedAuto") as WeaponMode, + beamWidth: itemNumber((item as AddItem).beamWidth, 1), + beamFullWidthRange: itemNumber((item as AddItem).beamFullWidthRange, 18), + customBeamSoundDef: (item as AddItem).customBeamSoundDef || "" + })); + next.scenario.startingPawnRaceMode = next.scenario.startingPawnRaceMode || "stableSelectedOnly"; + next.race.preserveTextureColors = Boolean(next.race.preserveTextureColors); + return next; +} + +function uid(prefix: string) { + return `${prefix}_${Math.random().toString(36).slice(2, 9)}`; +} + +function safeJson(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +function summarizeForLog(value: unknown): string { + if (value === undefined) return "undefined"; + if (value === null) return "null"; + if (typeof value === "string") return value.length > 120 ? `${value.slice(0, 117)}...` : value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + if (Array.isArray(value)) { + if (value.length <= 8 && value.every((v) => ["string", "number", "boolean"].includes(typeof v))) return `[${value.join(", ")}]`; + return `Array(${value.length})`; + } + if (typeof value === "object") { + const record = value as Record; + if ("fileName" in record) return `file:${String(record.fileName)}`; + const keys = Object.keys(record).filter((k) => k !== "dataUrl"); + return `{${keys.slice(0, 6).join(", ")}${keys.length > 6 ? ", ..." : ""}}`; + } + return safeJson(value); +} + +function makeLogEntry(action: string, detail: string, scope = "project"): LogEntry { + return { id: uid("log"), timestamp: new Date().toISOString(), scope, action, detail }; +} + +function valuesEqualForLog(a: unknown, b: unknown): boolean { + return safeJson(a) === safeJson(b); +} + +function diffLogEntries(scope: string, before: unknown, after: unknown): LogEntry[] { + if (valuesEqualForLog(before, after)) return []; + if (before && after && typeof before === "object" && typeof after === "object" && !Array.isArray(before) && !Array.isArray(after)) { + const beforeObj = before as Record; + const afterObj = after as Record; + const keys = Array.from(new Set([...Object.keys(beforeObj), ...Object.keys(afterObj)])); + const entries: LogEntry[] = []; + for (const key of keys) { + if (key === "logs" || key === "assets" || key === "dataUrl") continue; + if (!valuesEqualForLog(beforeObj[key], afterObj[key])) { + entries.push(makeLogEntry("Updated", `${scope}.${key}: ${summarizeForLog(beforeObj[key])} -> ${summarizeForLog(afterObj[key])}`, scope)); + } + } + return entries; + } + return [makeLogEntry("Updated", `${scope}: ${summarizeForLog(before)} -> ${summarizeForLog(after)}`, scope)]; +} + +function emitEditorLogs(entries: LogEntry[]) { + if (!entries.length) return; + for (const entry of entries) console.log(`[ModMaker][${entry.timestamp}][${entry.scope}] ${entry.action}: ${entry.detail}`); + void fetch("/__modmaker/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ entries }) + }).catch(() => {}); +} + +function sanitizeLogFileName(value: string): string { + return value.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "modmaker_log"; +} + +function formatTimestampForFile(date = new Date()) { + return date.toISOString().replace(/[:.]/g, "-"); +} + +function formatProjectLog(project: Project, extraEntries: LogEntry[] = [], validation: string[] = [], generatedFiles: string[] = []): string { + const entries = [...(project.logs ?? []), ...extraEntries]; + const features = [ + project.race.enabled ? "Custom Race" : null, + project.faction.enabled ? "FactionDef" : null, + project.storyteller.enabled ? "StorytellerDef" : null, + project.scenario.enabled ? "ScenarioDef" : null, + project.items.length ? `Add Item x${project.items.length}` : null, + project.research.length ? `ResearchProjectDef x${project.research.length}` : null + ].filter(Boolean).join(", ") || "About.xml only"; + return [ + "RimWorld Visual Mod Maker - Project Log", + "============================================================", + `Generated at: ${new Date().toISOString()}`, + `Schema: ${project.schemaVersion}`, + `Mod name: ${project.mod.name}`, + `Package ID: ${project.mod.packageId}`, + `Author: ${project.mod.author}`, + `Enabled features: ${features}`, + "", + "Validation", + "------------------------------------------------------------", + validation.length ? validation.map((v) => `- ${v}`).join("\n") : "No blocking validation errors.", + "", + "Generated files", + "------------------------------------------------------------", + generatedFiles.length ? generatedFiles.map((f) => `- ${f}`).join("\n") : "No generated file list captured.", + "", + "Change log", + "------------------------------------------------------------", + entries.length ? entries.map((e) => `[${e.timestamp}] [${e.scope}] ${e.action}: ${e.detail}`).join("\n") : "No recorded edits in this session/project.", + "" + ].join("\n"); +} + +async function writeEditorLogFile(fileName: string, content: string) { + await fetch("/__modmaker/export-log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileName, content }) + }).catch(() => console.warn("Editor log folder endpoint unavailable; log could not be written to the editor logs/ folder.")); +} + +async function writeEditorSourceFile(fileName: string, content: string) { + await fetch("/__modmaker/export-source", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ fileName, content }) + }).catch(() => console.warn("Editor source folder endpoint unavailable; project source could not be written to the editor source/ folder.")); +} + +function escapeXml(value: string | number | boolean | undefined): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function sanitizeDefName(value: string): string { + const cleaned = value.replace(/[^A-Za-z0-9_]/g, ""); + const safe = /^[A-Za-z]/.test(cleaned) ? cleaned : `Test${cleaned}`; + return /\d$/.test(safe) ? `${safe}Def` : safe; +} + +function packageIdFromName(name: string) { + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, ".").replace(/^\.+|\.+$/g, "") || "testmod"; + return `test.${slug}`; +} + +function assetKey(parts: string[]) { + return parts.join("."); +} + +async function fileToAsset(file: File, label: string): Promise { + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result)); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); + return { id: uid("asset"), label, fileName: file.name, mimeType: file.type || "image/png", dataUrl }; +} + +async function dataUrlToBytes(dataUrl: string): Promise { + const response = await fetch(dataUrl); + return new Uint8Array(await response.arrayBuffer()); +} + +function textFile(path: string, content: string) { + return { path, content, binary: false as const }; +} + +function binaryFile(path: string, content: Uint8Array) { + return { path, content, binary: true as const }; +} + +type VirtualFile = ReturnType | ReturnType; + +function buildAboutXml(project: Project): string { + const deps = [...project.dependencies]; + if (project.race.enabled && !deps.some((d) => d.packageId === HAR_DEP.packageId)) deps.push(HAR_DEP); + const modDependencies = deps.filter((d) => d.required).map((d) => `
  • \n ${escapeXml(d.packageId)}\n ${escapeXml(d.displayName)}${d.steamWorkshopUrl ? `\n ${escapeXml(d.steamWorkshopUrl)}` : ""}${d.downloadUrl ? `\n ${escapeXml(d.downloadUrl)}` : ""}\n
  • `).join("\n"); + const loadAfter = deps.map((d) => `
  • ${escapeXml(d.packageId)}
  • `).join("\n"); + return `\n\n ${escapeXml(project.mod.packageId)}\n ${escapeXml(project.mod.name)}\n ${escapeXml(project.mod.author)}\n ${escapeXml(project.mod.description)}\n \n${csvList(project.mod.supportedVersionsCsv ?? project.mod.supportedVersions.join(",")).map((v) => `
  • ${escapeXml(v)}
  • `).join("\n")}\n
    ${modDependencies ? `\n \n${modDependencies}\n \n \n${loadAfter}\n ` : ""}\n
    \n`; +} + +function textureBase(project: Project, folder: string, relativePath: string) { + return `${project.mod.packageId}/${folder}/${relativePath}`; +} + +function directionSuffix(direction: Direction) { + if (direction === "front") return "south"; + if (direction === "side") return "east"; + if (direction === "back") return "north"; + return "single"; +} + +function getAsset(project: Project, key: string) { + return project.assets[key]; +} + +async function addDirectionalTextures(files: VirtualFile[], project: Project, keyBase: string, exportBase: string, filePrefix: string) { + const directions: Direction[] = ["front", "side", "back"]; + for (const dir of directions) { + const asset = getAsset(project, assetKey([keyBase, dir])); + if (!asset) continue; + const suffix = directionSuffix(dir); + const bytes = await dataUrlToBytes(asset.dataUrl); + files.push(binaryFile(`${exportBase}/${filePrefix}_${suffix}.png`, bytes)); + if (dir === "side") files.push(binaryFile(`${exportBase}/${filePrefix}_west.png`, bytes)); + } +} + +function buildRaceXml(project: Project): string { + const r = project.race; + const bodyBase = textureBase(project, "Races", `${r.defName}/Bodies/${r.defName}_Body`); + const headBase = textureBase(project, "Races", `${r.defName}/Heads/${r.defName}_Head`); + const femaleBodyBase = textureBase(project, "Races", `${r.defName}/Bodies/${r.defName}_Female_Body`); + const maleHeadBase = textureBase(project, "Races", `${r.defName}/Heads/${r.defName}_Male_Head`); + const femaleHeadBase = textureBase(project, "Races", `${r.defName}/Heads/${r.defName}_Female_Head`); + const headMale = r.headTextureMode === "gendered" ? maleHeadBase : headBase; + const headFemale = r.headTextureMode === "gendered" ? femaleHeadBase : headBase; + const cleanGenes = Array.from(new Set(r.genes.map((g) => GENE_ALIAS[g] ?? g))); + const genes = cleanGenes.map((g) => `
  • ${escapeXml(g)}
  • `).join("\n"); + const bodyPaths = r.textureMode === "gendered" ? `\n ${escapeXml(bodyBase)}\n \n ${escapeXml(femaleBodyBase)}\n ` : `\n ${escapeXml(bodyBase)}`; + + return `\n\n \n ${escapeXml(r.defName)}_Head_Male\n ${escapeXml(headMale)}\n Male\n \n \n ${escapeXml(r.defName)}_Head_Female\n ${escapeXml(headFemale)}\n Female\n \n \n ${escapeXml(r.defName)}\n \n ${escapeXml(r.description)}\n \n Human\n Humanlike\n OmnivoreHuman\n ${r.bodySize}\n ${r.healthScale}\n ${r.lifeExpectancy}\n true\n \n \n ${r.moveSpeed}\n \n \n
  • \n \n
  • Blunt
  • \n ${r.meleeDamage}\n 2\n \n
    \n \n \n \n \n
  • ${escapeXml(r.defName)}_Head_Male
  • \n
  • ${escapeXml(r.defName)}_Head_Female
  • \n
    \n
    ${genes ? `\n \n${genes}\n ` : ""}\n
    \n \n ${bodyPaths}\n \n \n
    \n
    \n \n ${escapeXml(r.xenotypeDefName)}\n \n ${escapeXml(r.xenotypeDescription)}\n UI/Icons/Xenotypes/Baseliner\n true${genes ? `\n \n${cleanGenes.map((g) => `
  • ${escapeXml(g)}
  • `).join("\n")}\n
    ` : ""}\n
    \n
    \n`; +} + +function pawnKindXmlBlock(defName: string, label: string, raceDef: string, xenotypeDef: string, combatPower: number, defaultFactionType?: string) { + return ` + ${escapeXml(defName)} + + ${escapeXml(raceDef)}${defaultFactionType ? ` + ${escapeXml(defaultFactionType)}` : ""} + ${combatPower} + 13~21 + 3~6 + + + <${escapeXml(xenotypeDef)} MayRequire="Ludeon.RimWorld.Biotech">999 + + + `; +} + +function buildPawnKindXml(project: Project): string { + const r = project.race; + const blocks = [ + // Some in-game tools and Character Editor pages list PawnKindDef entries rather than only Race ThingDefs. + // Provide a plain alias named exactly like the race defName so the custom race is easy to find. + pawnKindXmlBlock(r.defName, r.label, r.defName, r.xenotypeDefName, 45, "PlayerColony"), + pawnKindXmlBlock(`${r.defName}_Colonist`, r.label, r.defName, r.xenotypeDefName, 45, "PlayerColony") + ]; + return `\n\n${blocks.join("\n")}\n\n`; +} + +function buildFactionXml(project: Project): string { + const f = project.faction; + const r = project.race; + return `\n\n \n ${escapeXml(f.defName)}\n \n ${escapeXml(f.fixedName)}\n ${escapeXml(f.description)} ${escapeXml(f.culture)}\n ${escapeXml(f.categoryTag)}\n ${escapeXml(f.techLevel)}\n ${f.requiredCountAtGameStart}\n ${f.maxConfigurableAtWorldCreation}\n true\n ${escapeXml(r.defName)}_FactionMember\n \n \n <${escapeXml(r.xenotypeDefName)} MayRequire="Ludeon.RimWorld.Biotech">999\n \n \n \n \n ${escapeXml(r.defName)}_FactionMember\n \n ${escapeXml(r.defName)}\n 55\n 13~21\n 3~6\n \n \n <${escapeXml(r.xenotypeDefName)} MayRequire="Ludeon.RimWorld.Biotech">999\n \n \n \n\n`; +} + +function itemNumber(value: number | undefined, fallback: number): number { + return Number.isFinite(value as number) ? Number(value) : fallback; +} + +function statLine(name: string, value: number | undefined, fallback: number) { + return ` <${name}>${itemNumber(value, fallback)}`; +} + +function defaultExplosionRadiusForDamageDef(damageDef: string): number { + switch ((damageDef || "").toLowerCase()) { + case "bomb": return 2; + case "emp": return 2; + case "smoke": return 3; + case "flame": return 1.5; + default: return 0; + } +} + +function defaultProjectileTextureForDamageDef(damageDef: string): string { + // Stable vanilla projectile textures. Do not use Projectile_Explosive/Projectile_EMP; + // those are class-like names and are not valid vanilla texture paths. + switch ((damageDef || "").toLowerCase()) { + case "bomb": return "Things/Projectile/Grenade"; + case "flame": return "Things/Projectile/Grenade"; + case "smoke": return "Things/Projectile/Grenade"; + case "emp": return "Things/Projectile/Bullet_Big"; + case "stun": return "Things/Projectile/Bullet_Big"; + default: return "Things/Projectile/Bullet_Small"; + } +} + +function isExplosiveProjectileDamageDef(damageDef: string): boolean { + const normalized = (damageDef || "").toLowerCase(); + return ["bomb", "flame", "emp", "smoke", "stun"].includes(normalized); +} + +function projectileThingClassXml(damageDef: string, explosionRadius: number): string { + // Bomb/EMP/Flame/Smoke projectiles with a radius must use Projectile_Explosive. + // Otherwise the radius may show in targeting UI while the fired projectile behaves like a normal bullet. + return isExplosiveProjectileDamageDef(damageDef) && explosionRadius > 0 + ? ` + Projectile_Explosive` + : ""; +} + +function projectileFireSpawnXml(_damageDef: string, _chance: number): string { + // RimWorld 1.6 ProjectileProperties rejects chanceToStartFire and explosionSpawnChance in user logs. + // Keep the UI field in project data for future expansion, but export no fire-spawn XML here. + return ""; +} + + +function normalizeCommaText(value: string | undefined): string { + // UI input must stay fluid. We normalize Chinese punctuation to ASCII commas while typing, + // but do not strip leading/trailing commas here. This lets users type `tech1,` + // and continue typing the next token without the controlled input fighting the cursor. + return String(value ?? "") + .replace(/[,、;;\n\r]+/g, ",") + .replace(/\s*,\s*/g, ",") + .replace(/,+/g, ","); +} + +function csvList(value: string | undefined): string[] { + const seen = new Set(); + const result: string[] = []; + for (const raw of normalizeCommaText(value).split(",")) { + const token = raw.trim(); + if (!token || seen.has(token)) continue; + seen.add(token); + result.push(token); + } + return result; +} + +function csvArrayList(values: string[] | undefined): string[] { + return csvList((values ?? []).join(",")); +} + +function researchOwnership(r: ResearchProject): ResearchTreeOwnership { + return r.techTreeOwnership ?? "vanilla"; +} + +function projectDefaultResearchTab(project: Project): string { + return sanitizeDefName(`${project.mod.name || "TestModName"}ResearchTab`); +} + +function researchTabDefName(project: Project, r: ResearchProject): string { + return sanitizeDefName(r.researchTabDefName || projectDefaultResearchTab(project)); +} + +function xmlList(tag: string, values: string[]): string { + if (!values.length) return ""; + return `\n <${tag}>\n${values.map((v) => `
  • ${escapeXml(v)}
  • `).join("\n")}\n `; +} + +function boolXml(value: boolean | undefined): string { + return value ? "true" : "false"; +} + +function researchPrerequisiteListXml(single: string | undefined): string { + const values = csvList(single); + if (!values.length) return ""; + return ` + +${values.map((v) => `
  • ${escapeXml(v)}
  • `).join("\n")} +
    `; +} + +function buildingCostXml(item: AddItem, steelCost: number, componentCost: number): string { + const stuffCategories = csvList(item.stuffCategoriesCsv || "Metallic"); + if (item.madeFromStuff) { + return ` + +${stuffCategories.map((v) => `
  • ${escapeXml(v)}
  • `).join("\n")} +
    + ${Math.max(1, Math.round(itemNumber(item.costStuffCount, 50)))}`; + } + const costEntries = [steelCost > 0 ? ` ${steelCost}` : "", componentCost > 0 ? ` ${componentCost}` : ""].filter(Boolean).join("\n") || " 1"; + return ` + +${costEntries} + `; +} + +function buildItemXml(project: Project, item: AddItem): string { + const pathBase = `${project.mod.packageId}/Items/${item.kind}/${item.defName}/${item.defName}`; + const marketValue = itemNumber(item.marketValue, 100); + const mass = itemNumber(item.mass, 1); + const flammability = itemNumber(item.flammability, 0.5); + const deteriorationRate = itemNumber(item.deteriorationRate, 2); + const beauty = itemNumber(item.beauty, -3); + const workToMake = itemNumber(item.workToMake, 2000); + const steelCost = Math.max(0, Math.round(itemNumber(item.steelCost, 50))); + const componentCost = Math.max(0, Math.round(itemNumber(item.componentCost, 0))); + const thingCategories = xmlList("thingCategories", csvList(item.thingCategoriesCsv)); + const tradeTags = xmlList("tradeTags", csvList(item.tradeTagsCsv)); + const common = ` ${escapeXml(item.defName)}\n \n ${escapeXml(item.description)}\n \n ${escapeXml(pathBase)}\n Graphic_Single\n (1,1)\n \n \n ${marketValue}\n ${mass}\n ${flammability}\n ${deteriorationRate}\n ${beauty}${item.kind === "weapon" ? `\n ${workToMake}` : ""}\n ${thingCategories}${tradeTags}`; + if (item.kind === "food") { + return `\n\n \n${common}\n ${item.stackLimit}\n \n Meal\n 0.9\n \n \n\n`; + } + if (item.kind === "building") { + const sizeX = Math.max(1, Math.round(itemNumber(item.buildingSizeX, 1))); + const sizeY = Math.max(1, Math.round(itemNumber(item.buildingSizeY, 1))); + const fillPercent = Math.max(0, Math.min(1, itemNumber(item.buildingFillPercent, 0.4))); + const maxHitPoints = Math.max(1, Math.round(itemNumber(item.buildingMaxHitPoints, 100))); + const designationCategory = sanitizeDefName(item.buildingDesignationCategory || "Furniture"); + const passability = item.buildingPassability || "PassThroughOnly"; + const costXml = buildingCostXml(item, steelCost, componentCost); + return `\n\n \n ${escapeXml(item.defName)}\n \n ${escapeXml(item.description)}\n Building\n Building\n MapMeshOnly\n Building\n \n ${escapeXml(pathBase)}\n Graphic_Single\n (${sizeX},${sizeY})\n Cutout\n \n (${sizeX},${sizeY})\n ${escapeXml(passability)}\n ${passability === "Standable" ? 0 : 50}\n ${fillPercent}\n true\n true\n true\n ${escapeXml(designationCategory)}\n Light\n \n ${maxHitPoints}\n ${marketValue}\n ${workToMake}\n ${flammability}\n ${beauty}\n ${costXml}${researchPrerequisiteListXml(item.researchPrerequisite)}\n \n\n`; + } + if (item.kind === "weapon") { + const isMelee = item.weaponMode === "melee"; + const isBeam = item.weaponMode === "beamLaser"; + const parent = isMelee ? "BaseMeleeWeapon_Sharp" : "BaseHumanMakeableGun"; + const sound = WEAPON_SOUND_PRESETS[item.soundPreset] ?? (isBeam ? WEAPON_SOUND_PRESETS.BeamSilent : WEAPON_SOUND_PRESETS.AssaultRifle); + const damage = itemNumber(item.damage, 10); + const cooldown = itemNumber(item.cooldown, 1.5); + const meleeArmor = itemNumber(item.meleeArmorPenetration, 0.25); + const rangedArmor = itemNumber(item.armorPenetration, 0.16); + const range = itemNumber(item.range, 28); + const warmupTime = itemNumber(item.warmupTime, 1.2); + const burstShotCount = (item.weaponMode === "rangedAuto" || item.weaponMode === "beamLaser") ? Math.max(1, Math.round(itemNumber(item.burstShotCount, item.weaponMode === "beamLaser" ? 30 : 6))) : 1; + const ticksBetweenBurstShots = (item.weaponMode === "rangedAuto" || item.weaponMode === "beamLaser") ? Math.max(1, Math.round(itemNumber(item.ticksBetweenBurstShots, item.weaponMode === "beamLaser" ? 2 : 8))) : 10; + const projectileSpeed = itemNumber(item.projectileSpeed, 70); + const accuracyTouch = itemNumber(item.accuracyTouch, 0.6); + const accuracyShort = itemNumber(item.accuracyShort, 0.8); + const accuracyMedium = itemNumber(item.accuracyMedium, 0.65); + const accuracyLong = itemNumber(item.accuracyLong, 0.45); + const projectileDef = `${item.defName}_Projectile`; + const damageDef = isBeam ? (item.damageDef && item.damageDef !== "Bullet" ? item.damageDef : "Beam") : (item.damageDef || "Bullet"); + const defaultProjectileTexPath = defaultProjectileTextureForDamageDef(damageDef); + const legacyInvalidProjectilePaths = ["Things/Projectile/Projectile_Explosive", "Things/Projectile/Projectile_EMP", "Things/Projectile/Projectile_Smoke"]; + const hasCustomProjectileTexture = Boolean(item.projectileGraphicPath && item.projectileGraphicPath !== "Things/Projectile/Bullet_Small" && item.projectileGraphicPath !== defaultProjectileTexPath && !legacyInvalidProjectilePaths.includes(item.projectileGraphicPath)); + const projectileTexPath = hasCustomProjectileTexture ? item.projectileGraphicPath : defaultProjectileTexPath; + const explosionRadiusInput = Math.max(0, itemNumber(item.projectileExplosionRadius, 0)); + const explosionRadius = explosionRadiusInput > 0 ? explosionRadiusInput : defaultExplosionRadiusForDamageDef(damageDef); + const stoppingPower = Math.max(0, itemNumber(item.projectileStoppingPower, 0.5)); + const chanceToStartFire = Math.max(0, itemNumber(item.projectileChanceToStartFire, 0)); + const projectileIsExplosive = isExplosiveProjectileDamageDef(damageDef) && explosionRadius > 0; + const explosiveVerbXml = projectileIsExplosive ? ` + ${Math.max(0.5, Math.min(3, explosionRadius * 0.5)).toFixed(1)} + ${Math.ceil(explosionRadius + 2)} + + true + ` : ""; + const weaponTags = xmlList("weaponTags", csvList(item.weaponTagsCsv)); + const recipeUsers = csvList(item.recipeUsersCsv || "FueledSmithy,ElectricSmithy"); + const recipeUsersXml = recipeUsers.length ? `${recipeUsers.map((v) => `
  • ${escapeXml(v)}
  • `).join("")}
    ` : ""; + const stuffCategories = csvList(item.stuffCategoriesCsv || "Metallic"); + const stuffXml = item.madeFromStuff ? `\n \n${stuffCategories.map((v) => `
  • ${escapeXml(v)}
  • `).join("\n")}\n
    \n ${Math.max(1, Math.round(itemNumber(item.costStuffCount, 50)))}` : ""; + const costEntries = [steelCost > 0 ? ` ${steelCost}` : "", componentCost > 0 ? ` ${componentCost}` : ""].filter(Boolean).join("\n") || " 1"; + const costXml = item.madeFromStuff ? stuffXml : `\n \n${costEntries}\n `; + const rangedStats = isMelee ? "" : `\n ${accuracyTouch}\n ${accuracyShort}\n ${accuracyMedium}\n ${accuracyLong}\n ${cooldown}`; + const weaponStatBases = ` \n ${marketValue}\n ${mass}\n ${flammability}\n ${deteriorationRate}\n ${beauty}\n ${workToMake}${rangedStats}\n `; + const meleeTool = `\n \n
  • \n \n
  • ${isMelee ? "Cut" : "Blunt"}
  • \n ${isMelee ? damage : Math.max(1, Math.round(damage * 0.35))}\n ${meleeArmor}\n ${isMelee ? cooldown : 2}\n \n
    `; + const beamWidth = Math.max(0.1, itemNumber(item.beamWidth, 1)); + const beamFullWidthRange = Math.max(0.1, itemNumber(item.beamFullWidthRange, Math.max(1, range * 0.65))); + const beamSoundDef = sanitizeDefName(item.customBeamSoundDef || sound.beamSoundCast || ""); + const beamSoundXml = beamSoundDef ? ` + ${escapeXml(beamSoundDef)}` : ""; + const beamTailXml = sound.soundCastTail ? ` + ${escapeXml(sound.soundCastTail)}` : ""; + const beamVisualPreset = item.beamVisualPreset || "anomalyIncinerator"; + const beamGroundFleckDef = sanitizeDefName(item.beamGroundFleckDef || "Fleck_IncineratorBeamBurn"); + const beamLineFleckDef = sanitizeDefName(item.beamLineFleckDef || "Fleck_IncineratorBeamSmoke"); + const beamEndEffecterDef = sanitizeDefName(item.beamEndEffecterDef || "IncineratorBeam_End"); + const beamVisualXml = (() => { + if (beamVisualPreset === "safeNone") return ""; + const mayRequire = beamVisualPreset === "anomalyIncinerator" ? ' MayRequire="Ludeon.RimWorld.Anomaly"' : ""; + const ground = beamGroundFleckDef ? ` + ${escapeXml(beamGroundFleckDef)} + 0.35` : ""; + const line = beamLineFleckDef ? ` + ${escapeXml(beamLineFleckDef)} + + +
  • (0, 0)
  • +
  • (0.65, 0.04)
  • +
  • (1, 0.45)
  • +
    +
    ` : ""; + const end = beamEndEffecterDef ? ` + ${escapeXml(beamEndEffecterDef)}` : ""; + return `${ground}${line}${end}`; + })(); + // Verb_ShootBeam uses highlightColor / secondaryHighlightColor for the visible beam core. + // Fleck/effecter fields only create hit smoke/burn particles, so without these colors the weapon + // can deal Beam damage while appearing to have no laser line. + const beamColorXml = isBeam ? ` + (80, 195, 255) + (10, 210, 220)` : ""; + const regularSoundCast = sound.soundCast || WEAPON_SOUND_PRESETS.AssaultRifle.soundCast || "Shot_AssaultRifle"; + const regularSoundTail = sound.soundCastTail || WEAPON_SOUND_PRESETS.AssaultRifle.soundCastTail || "GunTail_Medium"; + const verbs = isMelee ? "" : (isBeam ? ` + +
  • + Verb_ShootBeam + true + ${warmupTime} + ${range} + ${beamFullWidthRange} + ${beamWidth} + ${burstShotCount} + ${ticksBetweenBurstShots} + false + true + ${escapeXml(damageDef)}${beamSoundXml}${beamTailXml}${beamVisualXml}${beamColorXml} + 9 + 0.5 + 0 + 0.5 + true + true + + true + + Combat_RangedFire +
  • +
    ` : ` + +
  • + Verb_Shoot + true + ${escapeXml(projectileDef)} + ${warmupTime} + ${range} + ${burstShotCount} + ${ticksBetweenBurstShots}${explosiveVerbXml} + ${escapeXml(regularSoundCast)} + ${escapeXml(regularSoundTail)} + ${item.soundPreset === "Minigun" ? 12 : 9} +
  • +
    `); + const projectileExtra = `${explosionRadius > 0 ? ` + ${explosionRadius}` : ""}${projectileFireSpawnXml(damageDef, chanceToStartFire)}`; + const projectileThingClass = projectileThingClassXml(damageDef, explosionRadius); + const projectileGraphicShader = isExplosiveProjectileDamageDef(damageDef) ? "\n TransparentPostLight" : ""; + const projectileXml = (isMelee || isBeam) ? "" : ` + + ${escapeXml(projectileDef)} + ${projectileThingClass} + + ${escapeXml(projectileTexPath)} + Graphic_Single${projectileGraphicShader} + + + ${escapeXml(damageDef)} + ${damage} + ${rangedArmor} + ${projectileSpeed} + ${stoppingPower}${projectileExtra} + + `; + return `\n\n \n ${escapeXml(item.defName)}\n \n ${escapeXml(item.description)}\n \n ${escapeXml(pathBase)}\n Graphic_Single\n (1,1)\n \n${weaponStatBases}${xmlList("thingCategories", csvList(item.thingCategoriesCsv))}${xmlList("tradeTags", csvList(item.tradeTagsCsv))}${weaponTags}${meleeTool}${verbs}${costXml}\n \n ${recipeUsersXml}${item.researchPrerequisite ? `\n ${escapeXml(item.researchPrerequisite)}` : ""}\n ${workToMake}\n \n ${projectileXml}\n\n`; + } + if (item.kind === "apparel") { + return `\n\n \n${common}\n \n
  • Torso
  • \n ${escapeXml(pathBase)}\n
  • Shell
  • \n
    \n
    \n
    \n`; + } + if (item.kind === "hair") { + return `\n\n \n ${escapeXml(item.defName)}\n \n ${escapeXml(item.description)}\n ${escapeXml(pathBase)}\n
  • Urban
  • \n
    \n
    \n`; + } + return `\n\n \n${common}\n ${item.stackLimit}\n \n\n`; +} + +function buildResearchXml(project: Project): string { + if (project.research.length === 0) return ""; + const independentTabs = Array.from(new Map(project.research + .filter((r) => researchOwnership(r) === "independent") + .map((r) => { + const tabDef = researchTabDefName(project, r); + return [tabDef, { defName: tabDef, label: r.researchTabDefName ? r.researchTabDefName : `${project.mod.name} research` }]; + })).values()); + const tabsXml = independentTabs.map((tab) => ` + ${escapeXml(tab.defName)} + + `).join("\n"); + const researchXml = project.research.map((r, index) => { + const ownership = researchOwnership(r); + const tab = ownership === "independent" ? researchTabDefName(project, r) : "Main"; + const x = itemNumber(r.researchViewX, index + 1); + const y = itemNumber(r.researchViewY, 1); + const prereqs = csvList(r.prerequisitesCsv ?? r.prerequisites.join(",")); + return ` + ${escapeXml(r.defName)} + + ${escapeXml(r.description)} + ${r.baseCost} + ${r.techLevel} + ${escapeXml(tab)} + ${x} + ${y}${prereqs.length ? ` + +${prereqs.map((p) => `
  • ${escapeXml(p)}
  • `).join("\n")} +
    ` : ""} +
    `; + }).join("\n"); + return ` + +${tabsXml ? `${tabsXml}\n` : ""}${researchXml} + +`; +} + +function buildStorytellerXml(project: Project): string { + const s = project.storyteller; + const portraitLarge = project.assets["storyteller.large"] ? `\n ${escapeXml(project.mod.packageId)}/Storytellers/${escapeXml(s.defName)}` : ""; + const portraitTiny = project.assets["storyteller.tiny"] ? `\n ${escapeXml(project.mod.packageId)}/Storytellers/${escapeXml(s.defName)}_Tiny` : ""; + return `\n\n \n ${escapeXml(s.defName)}\n \n ${escapeXml(s.description)}\n ${s.listOrder}${portraitLarge}${portraitTiny}\n \n\n`; +} + +function buildScenarioXml(project: Project): string { + const s = project.scenario; + const playerFactionDef = s.playerFactionDef || "PlayerColony"; + const startingPawnPart = buildStartingPawnScenarioPart(project); + return ` + + + ${escapeXml(s.defName)} + + ${escapeXml(s.description)} + + ${escapeXml(s.summary)} + + PlayerFaction + ${escapeXml(playerFactionDef)} + + + SurfaceLayerFixed + Surface + Surface + Surface + true + + + ZoomOut + + + + +
  • + PlanetLayer + Orbit + Orbit + Orbit + true + + ZoomIn + +
  • +${startingPawnPart} +
  • + PlayerPawnsArriveMethod + Standing +
  • + ${scenarioThing("Silver", s.startWithSilver)} + ${scenarioThing("MealSurvivalPack", s.startWithPackagedMeals)} + ${scenarioThing("MedicineIndustrial", s.startWithMedicine)} + ${scenarioThing("ComponentIndustrial", s.startWithComponents)} + ${scenarioThing("Steel", s.startWithSteel)} +
    +
    +
    +
    +`; +} + +function buildStartingPawnScenarioPart(project: Project): string { + const s = project.scenario; + const forceCustomRace = Boolean(project.race.enabled && (s.forceCustomRaceStartingPawns ?? true)); + if (!forceCustomRace) { + return `
  • + ConfigPage_ConfigureStartingPawns + ${s.startingPawnCount} + ${s.chooseFromPawnCount} +
  • `; + } + const r = project.race; + const pawnKind = `${r.defName}_Colonist`; + const pawnCount = Math.max(1, Math.floor(Number(s.startingPawnCount) || 1)); + const choiceCount = Math.max(pawnCount, Math.floor(Number(s.chooseFromPawnCount) || pawnCount)); + const mode = s.startingPawnRaceMode || "stableSelectedOnly"; + const xenotypeCount = mode === "experimentalCandidatePool" ? choiceCount : pawnCount; + const actualChoiceCount = mode === "experimentalCandidatePool" ? choiceCount : Math.max(pawnCount, choiceCount); + // Important limitation: RimWorld's ScenPart_ConfigPage_ConfigureStartingPawns_Xenotypes only exposes + // pawnChoiceCount plus xenotypeCounts. Its count field controls how many pawns the scenario forces for + // that xenotype, not a separate hidden reserve pool. Keeping count=startingPawnCount is the safest XML-only + // behavior: selected starting pawns use the custom PawnKind; reserve pawns may still be generated as Human. + // The experimental mode intentionally uses count=chooseFromPawnCount for users who want to test full-pool forcing. + return `
  • + ConfigurePawnsXenotypes + ${actualChoiceCount} + ${escapeXml(s.summary)} + +
  • + ${escapeXml(r.xenotypeDefName)} + ${escapeXml(pawnKind)} +
  • + + +
  • + ${escapeXml(r.xenotypeDefName)} + ${xenotypeCount} + ${escapeXml(r.xenotypeLabel || r.label)} + Adult +
  • +
    + `; +} + +function scenarioThing(thingDef: string, count: number) { + return `
  • \n StartingThing_Defined\n ${thingDef}\n ${count}\n
  • `; +} + +function buildValidation(project: Project): string[] { + const errors: string[] = []; + if (!project.mod.packageId.includes(".")) errors.push("Package ID should contain dots, for example test.examplemod."); + if (project.race.enabled) { + const bodyKeys = project.race.textureMode === "gendered" ? ["race.body.male", "race.body.female"] : ["race.body.shared"]; + const headKeys = project.race.headTextureMode === "gendered" ? ["race.head.male", "race.head.female"] : ["race.head.shared"]; + for (const key of [...bodyKeys, ...headKeys]) for (const d of ["front", "side", "back"]) if (!project.assets[`${key}.${d}`]) errors.push(`Missing ${key} ${d} PNG.`); + } + if (project.faction.enabled && !project.race.enabled) errors.push("FactionDef requires Custom Race to be enabled in this beta."); + for (const item of project.items) { + if (item.kind === "apparel" || item.kind === "hair") { + for (const d of ["front", "side", "back"]) if (!project.assets[`item.${item.id}.${d}`]) errors.push(`Missing ${item.defName} ${d} PNG.`); + } else if (!project.assets[`item.${item.id}.single`]) { + errors.push(`Missing ${item.defName} single PNG.`); + } + } + return errors; +} + +function itemHasRequiredTextures(project: Project, item: AddItem) { + if (item.kind === "apparel" || item.kind === "hair") return ["front", "side", "back"].every((d) => project.assets[`item.${item.id}.${d}`]); + return Boolean(project.assets[`item.${item.id}.single`]); +} + +async function compileProject(project: Project): Promise { + const files: VirtualFile[] = []; + const root = project.mod.name.replace(/[\\/:*?"<>|]/g, "_"); + const push = (file: VirtualFile) => files.push(textFile(`${root}/${file.path}`, typeof file.content === "string" ? file.content : "")); + files.push(textFile(`${root}/About/About.xml`, buildAboutXml(project))); + // Source/ModMakerProject.json is intentionally not packaged into the playable Mod ZIP. + // It is written separately to the editor source/ folder during export. + + if (project.race.enabled) { + files.push(textFile(`${root}/Defs/ThingDefs/Races/${project.race.defName}.xml`, buildRaceXml(project))); + files.push(textFile(`${root}/Defs/PawnKindDefs/${project.race.defName}_PawnKind.xml`, buildPawnKindXml(project))); + const raceBase = `${root}/Textures/${project.mod.packageId}/Races/${project.race.defName}`; + await addDirectionalTextures(files, project, "race.body.shared", `${raceBase}/Bodies`, `${project.race.defName}_Body`); + await addDirectionalTextures(files, project, "race.body.male", `${raceBase}/Bodies`, `${project.race.defName}_Body`); + await addDirectionalTextures(files, project, "race.body.female", `${raceBase}/Bodies`, `${project.race.defName}_Female_Body`); + for (const bt of BODY_TYPES) await addDirectionalTextures(files, project, `race.bodytype.${bt}`, `${raceBase}/Bodies`, `${project.race.defName}_${bt}_Body`); + await addDirectionalTextures(files, project, "race.head.shared", `${raceBase}/Heads`, `${project.race.defName}_Head`); + await addDirectionalTextures(files, project, "race.head.male", `${raceBase}/Heads`, `${project.race.defName}_Male_Head`); + await addDirectionalTextures(files, project, "race.head.female", `${raceBase}/Heads`, `${project.race.defName}_Female_Head`); + } + if (project.faction.enabled && project.race.enabled) files.push(textFile(`${root}/Defs/FactionDefs/${project.faction.defName}.xml`, buildFactionXml(project))); + if (project.research.length) files.push(textFile(`${root}/Defs/ResearchProjectDefs/ResearchProjects.xml`, buildResearchXml(project))); + if (project.storyteller.enabled) { + files.push(textFile(`${root}/Defs/StorytellerDefs/${project.storyteller.defName}.xml`, buildStorytellerXml(project))); + for (const key of ["storyteller.large", "storyteller.tiny"]) { + const asset = project.assets[key]; + if (asset) files.push(binaryFile(`${root}/Textures/${project.mod.packageId}/Storytellers/${project.storyteller.defName}${key.endsWith("tiny") ? "_Tiny" : ""}.png`, await dataUrlToBytes(asset.dataUrl))); + } + } + if (project.scenario.enabled) files.push(textFile(`${root}/Defs/ScenarioDefs/${project.scenario.defName}.xml`, buildScenarioXml(project))); + + for (const item of project.items) { + if (!itemHasRequiredTextures(project, item)) continue; + files.push(textFile(`${root}/Defs/AddItems/${item.defName}.xml`, buildItemXml(project, item))); + const base = `${root}/Textures/${project.mod.packageId}/Items/${item.kind}/${item.defName}`; + if (item.kind === "apparel" || item.kind === "hair") { + await addDirectionalTextures(files, project, `item.${item.id}`, base, item.defName); + } else { + const asset = project.assets[`item.${item.id}.single`]; + if (asset) files.push(binaryFile(`${base}/${item.defName}.png`, await dataUrlToBytes(asset.dataUrl))); + const projectileAsset = project.assets[`item.${item.id}.projectile`]; + if (item.kind === "weapon" && projectileAsset) files.push(binaryFile(`${root}/Textures/${project.mod.packageId}/Items/projectiles/${item.defName}_Projectile.png`, await dataUrlToBytes(projectileAsset.dataUrl))); + } + } + return files; +} + +function Field(props: { label: string; value: string; onChange: (v: string) => void; textarea?: boolean }) { + return