Compare commits
19
Commits
No files matched your search
@@ -4,3 +4,4 @@ Cargo.lock.bak
|
||||
www/dungeoncraft.wasm
|
||||
www/mq_js_bundle.js
|
||||
play/
|
||||
dist/
|
||||
Generated
+829
-4
@@ -14,6 +14,28 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
@@ -26,6 +48,27 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
@@ -38,6 +81,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
@@ -50,6 +99,16 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
@@ -59,6 +118,50 @@ dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"block2",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-avf-audio",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.4"
|
||||
@@ -68,11 +171,31 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dungeoncraft"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"macroquad",
|
||||
"rfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -115,10 +238,34 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e796cb6894e6418daacea84b28dd4f98d3b32c74d5bb452ba1ecfbf5ad8e84ca"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.5",
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.27.0"
|
||||
@@ -136,6 +283,12 @@ dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
@@ -149,12 +302,91 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.21",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
||||
dependencies = [
|
||||
"jni-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
@@ -167,6 +399,18 @@ version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
|
||||
|
||||
[[package]]
|
||||
name = "macroquad"
|
||||
version = "0.4.16"
|
||||
@@ -209,7 +453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbaea978d741f3b60ced666b06a2a1e4a2bc81cb8b9dd29f4a40965e1f59e65"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndk-sys",
|
||||
"ndk-sys 0.2.2",
|
||||
"objc-rs",
|
||||
"winapi",
|
||||
]
|
||||
@@ -234,12 +478,52 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -249,6 +533,28 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-rs"
|
||||
version = "0.2.8"
|
||||
@@ -258,19 +564,154 @@ dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-avf-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -295,6 +736,66 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
|
||||
dependencies = [
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"percent-encoding",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
@@ -322,7 +823,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -344,6 +845,39 @@ version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
@@ -355,6 +889,85 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.54.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.19+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.5+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -370,6 +983,81 @@ version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -386,12 +1074,149 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
|
||||
@@ -17,3 +17,8 @@ codegen-units = 1
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
# desktop builds: system file dialog (xdg portal on Linux) and sound output
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rfd = { version = "0.17", default-features = false, features = ["xdg-portal"] }
|
||||
cpal = "0.18"
|
||||
@@ -31,4 +31,6 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY www/ /usr/share/nginx/html/
|
||||
COPY --from=build /out/ /usr/share/nginx/html/
|
||||
COPY --from=standalone /out/standalone/ /usr/share/nginx/html/standalone/
|
||||
# packed once for gzip_static (nginx.conf)
|
||||
RUN cd /usr/share/nginx/html && gzip -k -9 dungeoncraft.wasm mq_js_bundle.js standalone/DungeonCraft.html
|
||||
EXPOSE 80
|
||||
+27
@@ -37,6 +37,33 @@ cp ~/.cargo/registry/src/*/macroquad-*/js/mq_js_bundle.js www/
|
||||
python3 -m http.server 8080 -d www # open http://localhost:8080
|
||||
```
|
||||
|
||||
### Desktop build
|
||||
|
||||
The game also runs as a native program (Linux, Windows, macOS), without server and multiplayer:
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
./target/release/dungeoncraft
|
||||
```
|
||||
|
||||
On Linux the build needs the ALSA headers (`alsa-lib` / `libasound2-dev`); the sound goes through ALSA (PipeWire and
|
||||
PulseAudio take it). The settings, the autosave, the campaign progress and the profile are kept as files in the
|
||||
data folder: `~/.local/share/dungeoncraft/` (Linux, or `$XDG_DATA_HOME/dungeoncraft`), `%APPDATA%\DungeonCraft`
|
||||
(Windows), `~/Library/Application Support/DungeonCraft` (macOS); `DUNGEONCRAFT_DATA` chooses another one.
|
||||
*Save Game* and saved campaigns land in its `saves/` folder; *Load Game* opens the system file dialog there (on
|
||||
Linux through the desktop portal), and dropping a file onto the window works as well.
|
||||
|
||||
Release packages are built by `python3 tools/build_desktop.py [linux] [windows] [macos]` into `dist/`:
|
||||
|
||||
- **Linux**: `DungeonCraft-<version>-x86_64.AppImage`, built against glibc 2.28 (distributions from about 2019 on).
|
||||
- **Windows**: `DungeonCraft-<version>-windows-x64.exe`, a single file for Windows 10 and newer, cross-compiled on Linux.
|
||||
- **macOS**: `DungeonCraft-<version>-macos.zip` with a universal `DungeonCraft.app` (Apple silicon and Intel); only on a
|
||||
Mac. It is signed ad hoc, not notarised: on first start open it with right click -> *Open*.
|
||||
|
||||
Linux and Windows need [zig](https://ziglang.org/) (`mise install zig@0.15` works) and
|
||||
`cargo install --locked cargo-zigbuild`, plus `rustup target add x86_64-pc-windows-gnu`; the AppImage tool is downloaded
|
||||
once into `~/.cache/dungeoncraft/`.
|
||||
|
||||
### Testing the game logic
|
||||
|
||||
The tests run natively: a headless simulation of whole games, a savegame round trip, rendering of all music modules and the audio engine:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Balance runs
|
||||
|
||||
Whole games played headless by the AI brain on every side (`src/balance.rs`), deterministic per seed:
|
||||
|
||||
```bash
|
||||
cargo test --release balance_ -- --ignored --nocapture --test-threads=3
|
||||
BALANCE_SEEDS=16 cargo test --release balance_difficulty -- --ignored --nocapture # more seeds
|
||||
BALANCE_PAIR=0,1 ... # one difficulty pairing only (0 easy, 1 medium, 2 hard)
|
||||
BALANCE_TRACE=1 ... # wave runs: a status line every two minutes
|
||||
```
|
||||
|
||||
- **balance_difficulty** – duels of two AI masters (112x112, no hero waves, 45 min); the sides swap
|
||||
every other seed. Undecided games count as draws.
|
||||
- **balance_talents** – four AI masters (medium), each held to one talent branch, the branches moving
|
||||
round the four starts; placement by survival and strength.
|
||||
- **balance_waves** – one AI master alone against the hero waves (96x96, 40 min).
|
||||
|
||||
## Findings and changes (2026-11-06)
|
||||
|
||||
First runs showed:
|
||||
- AI masters stopped growing after their build order (99 room tiles, 24 creatures from minute 10).
|
||||
- They mined one vein at a time and only below 1500 gold.
|
||||
- They fought hero waves piecemeal and never cast spells.
|
||||
- "Hard" attacked even when weaker (boldness 0.9) and was no stronger than "easy".
|
||||
|
||||
Changes:
|
||||
- AI masters keep building lairs and gardens up to 24 / 50 / 80 beds (easy / medium / hard), and a
|
||||
bigger treasury when the old one is nearly full.
|
||||
- They mine several veins (one per three imps) whenever gold is below 1.5 paydays + 1000.
|
||||
- Defence: with intruders in the dungeon the rally flag goes on them, the master casts lightning on
|
||||
them and heals his hurt creatures; an attack is called off to defend.
|
||||
- Boldness (own army / enemy army needed to attack): easy 1.0, medium 1.2, hard 1.35.
|
||||
- Builder talent: imps dig +15% and rooms cost -10% per level (was +10% / -6%).
|
||||
|
||||
Results after the changes:
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| one master vs waves: fallen of 8 (easy/medium/hard) | 2 / 3 / 4 | 2 / 2 / 2 |
|
||||
| duels, 16 seeds: easy vs medium | 6 : 6 | 3 : 6 |
|
||||
| medium vs hard | 5 : 6 (8 seeds 3 : 2) | 5 : 6 |
|
||||
| easy vs hard | 3 : 7 | 3 : 7 |
|
||||
| talents, average place (builder / warlord / warlock / merchant) | 3.3 / 1.9 / 3.0 / 1.9 | 3.1 / 2.1 / 2.5 / 2.3 |
|
||||
|
||||
Notes:
|
||||
- The noise is about ±2 wins at 16 seeds; medium vs medium ended 5 : 7.
|
||||
- The AI uses only lightning and heal, so Warlock is still worth more to a human player than the
|
||||
tournament shows.
|
||||
- Builder pays off where space, not gold, limits building; AI dungeons are bounded by their radius.
|
||||
- One map seed (1333) never brings heroes to the master. It shows as "wave 11, heroes 0" in the
|
||||
wave table and is a map outlier.
|
||||
@@ -58,10 +58,12 @@ creatures and heroes.
|
||||
| Units | Spawn | count, kind, level, side, where (the core, the trigger unit, a zone), optional name |
|
||||
| | Hero wave | count (sent at once) |
|
||||
| | Remove | side, zone (removes those units) |
|
||||
| World | Change ground | zone → floor, earth, rock, water, lava, gold (the core, portals, towers and the gate stay) |
|
||||
| World | Change ground | zone → floor, earth, rock, water, lava, gold, mushroom grove (the core, portals, towers and the gate stay) |
|
||||
| | Reveal | zone (lifts the fog of war) |
|
||||
| | Show | zone (moves the camera there) |
|
||||
| | Gold + / Mana + | amount (negative takes) |
|
||||
| | Weather above ground | fair, rain, fog or storm, for seconds (0: until a block changes it); it fades in softly. Only maps with land above ground have weather |
|
||||
| | Talent experience + | amount (experience for the player's master talents) |
|
||||
| Logic | If / else | condition; blocks inside |
|
||||
| | Repeat | times (0–100); blocks inside |
|
||||
| | Repeat until | condition, checked **before** each run (already true: the inside never runs); blocks inside |
|
||||
@@ -80,7 +82,7 @@ Every number field holds a **value**; the switch in front of it changes the valu
|
||||
side/kind), *Count* in a zone, *Random* (whole number between a and b, deterministic),
|
||||
*Calculation* (`+ - * /`, division by 0 gives 0),
|
||||
- conditions (for *If*): *Comparison* (`< <= = != >= >`), *and*, *or*, *not*, *Yes/no*,
|
||||
*Trigger unit is a* (kind).
|
||||
*Trigger unit is a* (kind), *Weather above ground is* (fair, rain, fog, storm).
|
||||
|
||||
Variables are numbers the mission remembers (saved with the game); a variable is known once some
|
||||
script of the mission sets it.
|
||||
|
||||
+6
-2
@@ -140,5 +140,9 @@ grid), guard and attack markers (shields and swords), savegames with the game st
|
||||
or trees with lightning every 6-20 s. Dimmed daylight, rain streaks, fog veils, flashes; a HUD label.
|
||||
6. **Named creatures** (done, v202611-00068, names.rs) - every creature gets a name from its kind's list in the
|
||||
language files (20 per kind, per language), a humorous epithet from level 5 and a grand one from level 8;
|
||||
names in the hover info, the creature overview and the messages. A creature of level 3+ that falls leaves a
|
||||
memorial stone on its master's ground (hover: who lies there; at most 24).
|
||||
names in the hover info, the creature overview and the messages. A creature of level 10 that falls leaves a
|
||||
memorial stone on its master's ground (hover: who lies there; at most 30 per map).
|
||||
7. **Hall of the fallen** (done, v202611-00073) - every named creature of level 3+ that falls is remembered
|
||||
with its level, heroes slain and how it fell (slain by a kind, lava, water, snare, lightning): in the game
|
||||
(creature overview "Fallen", or a click with the hand on a memorial stone) and over all games on the
|
||||
achievements page (kept in the profile, at most 200, merged with the server copy).
|
||||
@@ -264,6 +264,8 @@ val.medium = Střední
|
||||
val.high = Vysoká
|
||||
val.normal = Normální
|
||||
val.large = Velké
|
||||
val.small = Malé
|
||||
val.auto_plain = Automaticky
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera vpřed
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Odhalit
|
||||
blk.camera = Ukázat
|
||||
blk.gold = Zlato +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Počasí na povrchu
|
||||
blk.talentxp = Zkušenosti talentů +
|
||||
blk.if = Když
|
||||
blk.repeat = Opakovat
|
||||
blk.trigger = Přepnout spouštěč
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Odhalí mlhu války nad zónou.
|
||||
blk.help.camera = Přesune kameru na zónu.
|
||||
blk.help.gold = Dá (nebo vezme) zlato.
|
||||
blk.help.mana = Dá (nebo vezme) manu.
|
||||
blk.help.weather = Nastaví počasí na povrchu (jasno, déšť, mlha nebo bouře) na tolik sekund; 0 ho ponechá, dokud ho jiný blok nezmění.
|
||||
blk.help.talentxp = Dá hráči zkušenosti pro pánovy talenty.
|
||||
blk.help.if = Spustí vnitřní bloky, jen když platí podmínka, jinak ty pod "jinak".
|
||||
blk.help.repeat = Spustí vnitřní bloky několikrát.
|
||||
blk.help.trigger = Zapne spouštěč (může se spustit znovu) nebo ho vypne.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = nebo
|
||||
blk.v.not = ne
|
||||
blk.v.bool = Ano/ne
|
||||
blk.v.eventkind = Spouštěcí jednotka je
|
||||
blk.v.weatheris = Počasí na povrchu je
|
||||
blk.w_at = na
|
||||
blk.w_by = o
|
||||
blk.w_in = v
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Opakovat dokud
|
||||
blk.dountil = Opakovat, pak ověřit: dokud
|
||||
blk.wait = Čekat
|
||||
blk.w_seconds = sekund
|
||||
blk.w_for = na
|
||||
blk.help.until = Spouští vnitřní bloky znovu a znovu, dokud neplatí podmínka - ověřuje se před každým během, takže pokud už platí, vnitřek se nespustí vůbec.
|
||||
blk.help.dountil = Spustí vnitřní bloky, pak ověří podmínku a opakuje, dokud neplatí - vnitřek proběhne vždy aspoň jednou.
|
||||
blk.help.wait = Pozastaví tento skript na několik sekund, pak pokračuje dalším blokem (i uvnitř smyček).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Padl tvůj tvor: {0}. Pamětní kámen uchová jméno.
|
||||
tile.memorial = Pamětní kámen
|
||||
tile.memorial_info = Zde odpočívá {0} ({1}, úroveň {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Síň padlých
|
||||
hall.count = Padlí tvorové v paměti: {0}
|
||||
hall.empty = Zatím nikdo nepadl. Ať to tak vydrží.
|
||||
hall.kills = Zabití hrdinové: {0}
|
||||
ov.fallen = Padlí ({0})
|
||||
ov.living = Živí
|
||||
cause.slain = vrah: {0}
|
||||
cause.lava = smrt v lávě
|
||||
cause.water = utonutí
|
||||
cause.snare = smrt v pasti
|
||||
cause.lightning = zásah bleskem
|
||||
cause.fell = smrt v boji
|
||||
@@ -264,6 +264,8 @@ val.medium = Middel
|
||||
val.high = Høj
|
||||
val.normal = Normal
|
||||
val.large = Stor
|
||||
val.small = Lille
|
||||
val.auto_plain = Automatisk
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera frem
|
||||
@@ -1373,6 +1375,8 @@ blk.reveal = Afslør
|
||||
blk.camera = Vis
|
||||
blk.gold = Guld +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Vejret over jorden
|
||||
blk.talentxp = Talenterfaring +
|
||||
blk.if = Hvis
|
||||
blk.repeat = Gentag
|
||||
blk.trigger = Skift udløser
|
||||
@@ -1392,6 +1396,8 @@ blk.help.reveal = Løfter krigens tåge over en zone.
|
||||
blk.help.camera = Flytter kameraet til en zone.
|
||||
blk.help.gold = Giver (eller tager) guld.
|
||||
blk.help.mana = Giver (eller tager) mana.
|
||||
blk.help.weather = Sætter vejret over jorden (klart, regn, tåge eller storm) i så mange sekunder; 0 beholder det, til en anden blok ændrer det.
|
||||
blk.help.talentxp = Giver spilleren erfaring til mestertalenterne.
|
||||
blk.help.if = Kører blokkene indeni kun hvis betingelsen gælder, ellers dem under "ellers".
|
||||
blk.help.repeat = Kører blokkene indeni flere gange.
|
||||
blk.help.trigger = Slår en udløser til (den kan udløses igen) eller fra.
|
||||
@@ -1414,6 +1420,7 @@ blk.v.or = eller
|
||||
blk.v.not = ikke
|
||||
blk.v.bool = Ja/nej
|
||||
blk.v.eventkind = Udløserenhed er en
|
||||
blk.v.weatheris = Vejret over jorden er
|
||||
blk.w_at = ved
|
||||
blk.w_by = med
|
||||
blk.w_in = i
|
||||
@@ -1462,6 +1469,7 @@ blk.until = Gentag indtil
|
||||
blk.dountil = Gentag, tjek så: indtil
|
||||
blk.wait = Vent
|
||||
blk.w_seconds = sekunder
|
||||
blk.w_for = i
|
||||
blk.help.until = Kører blokkene indeni igen og igen, indtil betingelsen gælder - tjekkes før hver kørsel, så hvis den allerede gælder, kører indholdet slet ikke.
|
||||
blk.help.dountil = Kører blokkene indeni, tjekker så betingelsen og gentager, indtil den gælder - indholdet kører altid mindst én gang.
|
||||
blk.help.wait = Sætter dette script på pause i nogle sekunder, derefter fortsætter det med næste blok (også inde i løkker).
|
||||
@@ -1745,3 +1753,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} er faldet. En mindesten bevarer navnet.
|
||||
tile.memorial = Mindesten
|
||||
tile.memorial_info = Her hviler {0} ({1}, niveau {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = De faldnes hal
|
||||
hall.count = Faldne væsner i erindring: {0}
|
||||
hall.empty = Endnu er ingen faldet. Længe må det vare.
|
||||
hall.kills = Dræbte helte: {0}
|
||||
ov.fallen = Faldne ({0})
|
||||
ov.living = Levende
|
||||
cause.slain = dræbt af: {0}
|
||||
cause.lava = brændt i lava
|
||||
cause.water = druknet
|
||||
cause.snare = trådt i en saks
|
||||
cause.lightning = ramt af lynet
|
||||
cause.fell = faldet
|
||||
@@ -263,6 +263,8 @@ val.medium = Mittel
|
||||
val.high = Hoch
|
||||
val.normal = Normal
|
||||
val.large = Groß
|
||||
val.small = Klein
|
||||
val.auto_plain = Automatisch
|
||||
|
||||
# ---- Namen der Tastenbelegung
|
||||
key.cam_forward = Kamera vor
|
||||
@@ -1373,6 +1375,8 @@ blk.reveal = Aufdecken
|
||||
blk.camera = Zeigen
|
||||
blk.gold = Gold +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Wetter über der Erde
|
||||
blk.talentxp = Talent-Erfahrung +
|
||||
blk.if = Wenn
|
||||
blk.repeat = Wiederhole
|
||||
blk.trigger = Trigger schalten
|
||||
@@ -1392,6 +1396,8 @@ blk.help.reveal = Hebt den Nebel über einer Zone.
|
||||
blk.help.camera = Bewegt die Kamera zu einer Zone.
|
||||
blk.help.gold = Gibt (oder nimmt) Gold.
|
||||
blk.help.mana = Gibt (oder nimmt) Mana.
|
||||
blk.help.weather = Setzt das Wetter über der Erde (heiter, Regen, Nebel oder Sturm) für so viele Sekunden; 0 hält es, bis ein anderer Block es ändert.
|
||||
blk.help.talentxp = Gibt dem Spieler Erfahrung für die Meister-Talente.
|
||||
blk.help.if = Führt die Blöcke darin nur aus, wenn die Bedingung gilt, sonst die unter "sonst".
|
||||
blk.help.repeat = Führt die Blöcke darin mehrmals aus.
|
||||
blk.help.trigger = Schaltet einen Trigger ein (er darf erneut auslösen) oder aus.
|
||||
@@ -1414,6 +1420,7 @@ blk.v.or = oder
|
||||
blk.v.not = nicht
|
||||
blk.v.bool = Ja/Nein
|
||||
blk.v.eventkind = Auslöser ist ein
|
||||
blk.v.weatheris = Wetter über der Erde ist
|
||||
blk.w_at = bei
|
||||
blk.w_by = um
|
||||
blk.w_in = in
|
||||
@@ -1462,6 +1469,7 @@ blk.until = Wiederhole bis
|
||||
blk.dountil = Wiederhole, prüfe danach: bis
|
||||
blk.wait = Warte
|
||||
blk.w_seconds = Sekunden
|
||||
blk.w_for = für
|
||||
blk.help.until = Führt die Blöcke darin immer wieder aus, bis die Bedingung gilt - geprüft vor jedem Durchlauf; gilt sie schon, läuft der Inhalt gar nicht.
|
||||
blk.help.dountil = Führt die Blöcke darin aus, prüft dann die Bedingung und wiederholt, bis sie gilt - der Inhalt läuft mindestens einmal.
|
||||
blk.help.wait = Hält dieses Skript einige Sekunden an, danach geht es mit dem nächsten Block weiter (auch in Schleifen).
|
||||
@@ -1745,3 +1753,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} ist gefallen. Ein Gedenkstein bewahrt den Namen.
|
||||
tile.memorial = Gedenkstein
|
||||
tile.memorial_info = Hier ruht {0} ({1}, Stufe {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Halle der Gefallenen
|
||||
hall.count = {0} gefallene Kreaturen in Erinnerung
|
||||
hall.empty = Noch ist niemand gefallen. Möge es so bleiben.
|
||||
hall.kills = {0} Helden erschlagen
|
||||
ov.fallen = Gefallene ({0})
|
||||
ov.living = Lebende
|
||||
cause.slain = erschlagen von: {0}
|
||||
cause.lava = in Lava verbrannt
|
||||
cause.water = ertrunken
|
||||
cause.snare = in eine Falle getreten
|
||||
cause.lightning = vom Blitz getroffen
|
||||
cause.fell = gefallen
|
||||
@@ -264,6 +264,8 @@ val.medium = Μεσαία
|
||||
val.high = Υψηλή
|
||||
val.normal = Κανονικό
|
||||
val.large = Μεγάλο
|
||||
val.small = Μικρό
|
||||
val.auto_plain = Αυτόματο
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Κάμερα μπροστά
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Αποκάλυψη
|
||||
blk.camera = Δείξε
|
||||
blk.gold = Χρυσός +
|
||||
blk.mana = Μάνα +
|
||||
blk.weather = Καιρός στην επιφάνεια
|
||||
blk.talentxp = Εμπειρία ταλέντων +
|
||||
blk.if = Αν
|
||||
blk.repeat = Επανάληψη
|
||||
blk.trigger = Διακόπτης εναύσματος
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Σηκώνει την ομίχλη του πολέμου πά
|
||||
blk.help.camera = Μετακινεί την κάμερα σε μια ζώνη.
|
||||
blk.help.gold = Δίνει (ή παίρνει) χρυσό.
|
||||
blk.help.mana = Δίνει (ή παίρνει) μάνα.
|
||||
blk.help.weather = Ορίζει τον καιρό στην επιφάνεια (αίθριο, βροχή, ομίχλη ή καταιγίδα) για τόσα δευτερόλεπτα· το 0 τον κρατά μέχρι να τον αλλάξει άλλο μπλοκ.
|
||||
blk.help.talentxp = Δίνει στον παίκτη εμπειρία για τα ταλέντα του άρχοντα.
|
||||
blk.help.if = Εκτελεί τα μπλοκ μέσα μόνο αν ισχύει η συνθήκη, αλλιώς αυτά κάτω από το "αλλιώς".
|
||||
blk.help.repeat = Εκτελεί τα μπλοκ μέσα πολλές φορές.
|
||||
blk.help.trigger = Ενεργοποιεί ένα έναυσμα (μπορεί να ξαναπυροδοτηθεί) ή το απενεργοποιεί.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = ή
|
||||
blk.v.not = όχι
|
||||
blk.v.bool = Ναι/όχι
|
||||
blk.v.eventkind = Μονάδα εναύσματος είναι
|
||||
blk.v.weatheris = Ο καιρός στην επιφάνεια είναι
|
||||
blk.w_at = σε
|
||||
blk.w_by = κατά
|
||||
blk.w_in = σε
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Επανάληψη ώσπου
|
||||
blk.dountil = Επανάληψη, μετά έλεγχος: ώσπου
|
||||
blk.wait = Αναμονή
|
||||
blk.w_seconds = δευτερόλεπτα
|
||||
blk.w_for = για
|
||||
blk.help.until = Εκτελεί τα μπλοκ μέσα ξανά και ξανά ώσπου να ισχύσει η συνθήκη - ελέγχεται πριν από κάθε εκτέλεση, οπότε αν ισχύει ήδη, το εσωτερικό δεν εκτελείται καθόλου.
|
||||
blk.help.dountil = Εκτελεί τα μπλοκ μέσα, μετά ελέγχει τη συνθήκη και επαναλαμβάνει ώσπου να ισχύσει - το εσωτερικό εκτελείται πάντα τουλάχιστον μία φορά.
|
||||
blk.help.wait = Σταματά αυτό το σενάριο για μερικά δευτερόλεπτα, και μετά συνεχίζει με το επόμενο μπλοκ (και μέσα σε βρόχους).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Έπεσε: {0}. Μια αναμνηστική πέτρα κρατά το όνομα.
|
||||
tile.memorial = Αναμνηστική πέτρα
|
||||
tile.memorial_info = Εδώ αναπαύεται: {0} ({1}, επίπεδο {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Αίθουσα των πεσόντων
|
||||
hall.count = Πεσόντα πλάσματα στη μνήμη: {0}
|
||||
hall.empty = Κανείς δεν έχει πέσει ακόμη. Είθε να κρατήσει.
|
||||
hall.kills = Ήρωες που σκοτώθηκαν: {0}
|
||||
ov.fallen = Πεσόντες ({0})
|
||||
ov.living = Ζωντανοί
|
||||
cause.slain = φονιάς: {0}
|
||||
cause.lava = θάνατος στη λάβα
|
||||
cause.water = πνιγμός
|
||||
cause.snare = θάνατος σε παγίδα
|
||||
cause.lightning = χτύπημα κεραυνού
|
||||
cause.fell = θάνατος στη μάχη
|
||||
@@ -264,6 +264,8 @@ val.medium = Medium
|
||||
val.high = High
|
||||
val.normal = Normal
|
||||
val.large = Large
|
||||
val.small = Small
|
||||
val.auto_plain = Auto
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Camera forward
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Reveal
|
||||
blk.camera = Show
|
||||
blk.gold = Gold +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Weather above ground
|
||||
blk.talentxp = Talent experience +
|
||||
blk.if = If
|
||||
blk.repeat = Repeat
|
||||
blk.trigger = Switch trigger
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Lifts the fog of war over a zone.
|
||||
blk.help.camera = Moves the camera to a zone.
|
||||
blk.help.gold = Gives (or takes) gold.
|
||||
blk.help.mana = Gives (or takes) mana.
|
||||
blk.help.weather = Sets the weather above ground (fair, rain, fog or storm) for so many seconds; 0 keeps it until another block changes it.
|
||||
blk.help.talentxp = Gives the player experience for the master talents.
|
||||
blk.help.if = Runs the blocks inside only if the condition holds, otherwise the ones under "else".
|
||||
blk.help.repeat = Runs the blocks inside several times.
|
||||
blk.help.trigger = Switches a trigger on (it may fire again) or off.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = or
|
||||
blk.v.not = not
|
||||
blk.v.bool = Yes/no
|
||||
blk.v.eventkind = Trigger unit is a
|
||||
blk.v.weatheris = Weather above ground is
|
||||
blk.w_at = at
|
||||
blk.w_by = by
|
||||
blk.w_in = in
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Repeat until
|
||||
blk.dountil = Repeat, then check: until
|
||||
blk.wait = Wait
|
||||
blk.w_seconds = seconds
|
||||
blk.w_for = for
|
||||
blk.help.until = Runs the blocks inside again and again until the condition holds - checked before each run, so if it already holds the inside does not run at all.
|
||||
blk.help.dountil = Runs the blocks inside, then checks the condition and repeats until it holds - the inside always runs at least once.
|
||||
blk.help.wait = Pauses this script for some seconds, then it goes on with the next block (also inside loops).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} has fallen. A memorial stone keeps the name.
|
||||
tile.memorial = Memorial stone
|
||||
tile.memorial_info = Here lies {0} ({1}, level {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Hall of the fallen
|
||||
hall.count = {0} fallen creatures remembered
|
||||
hall.empty = No one has fallen yet. Long may it last.
|
||||
hall.kills = {0} heroes slain
|
||||
ov.fallen = Fallen ({0})
|
||||
ov.living = Living
|
||||
cause.slain = slain by: {0}
|
||||
cause.lava = burned in lava
|
||||
cause.water = drowned
|
||||
cause.snare = caught in a snare
|
||||
cause.lightning = struck by lightning
|
||||
cause.fell = fell
|
||||
@@ -264,6 +264,8 @@ val.medium = Medio
|
||||
val.high = Alto
|
||||
val.normal = Normal
|
||||
val.large = Grande
|
||||
val.small = Pequeño
|
||||
val.auto_plain = Automático
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Cámara adelante
|
||||
@@ -1366,6 +1368,8 @@ blk.reveal = Revelar
|
||||
blk.camera = Mostrar
|
||||
blk.gold = Oro +
|
||||
blk.mana = Maná +
|
||||
blk.weather = Clima en la superficie
|
||||
blk.talentxp = Experiencia de talentos +
|
||||
blk.if = Si
|
||||
blk.repeat = Repetir
|
||||
blk.trigger = Conmutar disparador
|
||||
@@ -1385,6 +1389,8 @@ blk.help.reveal = Levanta la niebla de guerra sobre una zona.
|
||||
blk.help.camera = Mueve la cámara a una zona.
|
||||
blk.help.gold = Da (o quita) oro.
|
||||
blk.help.mana = Da (o quita) maná.
|
||||
blk.help.weather = Fija el clima en la superficie (despejado, lluvia, niebla o tormenta) durante tantos segundos; 0 lo mantiene hasta que otro bloque lo cambie.
|
||||
blk.help.talentxp = Da al jugador experiencia para los talentos del maestro.
|
||||
blk.help.if = Ejecuta los bloques de dentro solo si se cumple la condición; si no, los de «si no».
|
||||
blk.help.repeat = Ejecuta varias veces los bloques de dentro.
|
||||
blk.help.trigger = Activa un disparador (puede volver a saltar) o lo desactiva.
|
||||
@@ -1407,6 +1413,7 @@ blk.v.or = o
|
||||
blk.v.not = no
|
||||
blk.v.bool = Sí/no
|
||||
blk.v.eventkind = El causante es un
|
||||
blk.v.weatheris = El clima en la superficie es
|
||||
blk.w_at = en
|
||||
blk.w_by = en
|
||||
blk.w_in = en
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Repetir hasta
|
||||
blk.dountil = Repetir y luego: hasta
|
||||
blk.wait = Esperar
|
||||
blk.w_seconds = segundos
|
||||
blk.w_for = durante
|
||||
blk.help.until = Ejecuta los bloques de dentro una y otra vez hasta que se cumpla la condición; se comprueba antes de cada vuelta, así que si ya se cumple no se ejecuta nada.
|
||||
blk.help.dountil = Ejecuta los bloques de dentro, luego comprueba la condición y repite hasta que se cumpla; el contenido se ejecuta al menos una vez.
|
||||
blk.help.wait = Detiene este script unos segundos y luego sigue con el siguiente bloque (también dentro de bucles).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} ha caído. Una piedra conmemorativa guarda su nombre.
|
||||
tile.memorial = Piedra conmemorativa
|
||||
tile.memorial_info = Aquí yace {0} ({1}, nivel {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Salón de los caídos
|
||||
hall.count = Criaturas caídas en el recuerdo: {0}
|
||||
hall.empty = Aún no ha caído nadie. Que siga así.
|
||||
hall.kills = Héroes abatidos: {0}
|
||||
ov.fallen = Caídos ({0})
|
||||
ov.living = Vivos
|
||||
cause.slain = muerte a manos de: {0}
|
||||
cause.lava = muerte en lava
|
||||
cause.water = ahogamiento
|
||||
cause.snare = muerte en un cepo
|
||||
cause.lightning = muerte por rayo
|
||||
cause.fell = caída en combate
|
||||
@@ -264,6 +264,8 @@ val.medium = Keski
|
||||
val.high = Korkea
|
||||
val.normal = Normaali
|
||||
val.large = Suuri
|
||||
val.small = Pieni
|
||||
val.auto_plain = Automaattinen
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera eteen
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Paljasta
|
||||
blk.camera = Näytä
|
||||
blk.gold = Kulta +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Sää maan päällä
|
||||
blk.talentxp = Kykykokemus +
|
||||
blk.if = Jos
|
||||
blk.repeat = Toista
|
||||
blk.trigger = Vaihda laukaisin
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Poistaa sodan sumun alueen yltä.
|
||||
blk.help.camera = Siirtää kameran alueelle.
|
||||
blk.help.gold = Antaa (tai ottaa) kultaa.
|
||||
blk.help.mana = Antaa (tai ottaa) manaa.
|
||||
blk.help.weather = Asettaa sään maan päällä (selkeää, sade, sumu tai myrsky) niin moneksi sekunniksi; 0 pitää sen, kunnes toinen palikka muuttaa sitä.
|
||||
blk.help.talentxp = Antaa pelaajalle kokemusta mestarin kykyihin.
|
||||
blk.help.if = Suorittaa sisällä olevat lohkot vain, jos ehto pätee, muuten "muuten"-kohdan lohkot.
|
||||
blk.help.repeat = Suorittaa sisällä olevat lohkot useita kertoja.
|
||||
blk.help.trigger = Kytkee laukaisimen päälle (se voi laueta uudelleen) tai pois.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = tai
|
||||
blk.v.not = ei
|
||||
blk.v.bool = Kyllä/ei
|
||||
blk.v.eventkind = Laukaisija on laji
|
||||
blk.v.weatheris = Sää maan päällä on
|
||||
blk.w_at = paikka
|
||||
blk.w_by = määrä
|
||||
blk.w_in = alue
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Toista kunnes
|
||||
blk.dountil = Toista, sitten tarkista: kunnes
|
||||
blk.wait = Odota
|
||||
blk.w_seconds = sekuntia
|
||||
blk.w_for = kesto
|
||||
blk.help.until = Suorittaa sisällä olevia lohkoja yhä uudelleen, kunnes ehto pätee - tarkistetaan ennen jokaista kierrosta, joten jos ehto jo pätee, sisältöä ei suoriteta lainkaan.
|
||||
blk.help.dountil = Suorittaa sisällä olevat lohkot, tarkistaa sitten ehdon ja toistaa, kunnes se pätee - sisältö suoritetaan aina vähintään kerran.
|
||||
blk.help.wait = Pysäyttää tämän skriptin muutamaksi sekunniksi, sitten se jatkaa seuraavasta lohkosta (myös silmukoiden sisällä).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} on kaatunut. Muistokivi säilyttää nimen.
|
||||
tile.memorial = Muistokivi
|
||||
tile.memorial_info = Tässä lepää {0} ({1}, taso {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Kaatuneiden sali
|
||||
hall.count = Muistettuja kaatuneita olentoja: {0}
|
||||
hall.empty = Kukaan ei ole vielä kaatunut. Kauan näin jatkukoon.
|
||||
hall.kills = Surmattuja sankareita: {0}
|
||||
ov.fallen = Kaatuneet ({0})
|
||||
ov.living = Elävät
|
||||
cause.slain = surmaaja: {0}
|
||||
cause.lava = paloi laavassa
|
||||
cause.water = hukkui
|
||||
cause.snare = astui ansaan
|
||||
cause.lightning = salama iski
|
||||
cause.fell = kaatui
|
||||
@@ -264,6 +264,8 @@ val.medium = Srednje
|
||||
val.high = Visoko
|
||||
val.normal = Normalno
|
||||
val.large = Veliko
|
||||
val.small = Malo
|
||||
val.auto_plain = Automatski
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera naprijed
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Otkrij
|
||||
blk.camera = Pokaži
|
||||
blk.gold = Zlato +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Vrijeme na površini
|
||||
blk.talentxp = Iskustvo talenata +
|
||||
blk.if = Ako
|
||||
blk.repeat = Ponovi
|
||||
blk.trigger = Prebaci okidač
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Podiže maglu rata nad zonom.
|
||||
blk.help.camera = Pomiče kameru na zonu.
|
||||
blk.help.gold = Daje (ili oduzima) zlato.
|
||||
blk.help.mana = Daje (ili oduzima) manu.
|
||||
blk.help.weather = Postavlja vrijeme na površini (vedro, kiša, magla ili oluja) na toliko sekundi; 0 ga zadržava dok ga drugi blok ne promijeni.
|
||||
blk.help.talentxp = Daje igraču iskustvo za gospodareve talente.
|
||||
blk.help.if = Pokreće unutarnje blokove samo ako uvjet vrijedi, inače one pod "inače".
|
||||
blk.help.repeat = Pokreće unutarnje blokove nekoliko puta.
|
||||
blk.help.trigger = Uključuje okidač (može se ponovo aktivirati) ili ga isključuje.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = ili
|
||||
blk.v.not = ne
|
||||
blk.v.bool = Da/ne
|
||||
blk.v.eventkind = Jedinica okidača je
|
||||
blk.v.weatheris = Vrijeme na površini je
|
||||
blk.w_at = na
|
||||
blk.w_by = za
|
||||
blk.w_in = u
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Ponavljaj dok
|
||||
blk.dountil = Ponovi, zatim provjeri: dok
|
||||
blk.wait = Čekaj
|
||||
blk.w_seconds = sekundi
|
||||
blk.w_for = na
|
||||
blk.help.until = Pokreće unutarnje blokove iznova i iznova dok uvjet ne vrijedi - provjerava se prije svakog prolaza, pa ako već vrijedi, unutrašnjost se uopće ne pokreće.
|
||||
blk.help.dountil = Pokreće unutarnje blokove, zatim provjerava uvjet i ponavlja dok ne vrijedi - unutrašnjost se uvijek pokrene barem jednom.
|
||||
blk.help.wait = Pauzira ovu skriptu nekoliko sekundi, a zatim nastavlja sljedećim blokom (i unutar petlji).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Palo je tvoje stvorenje: {0}. Spomen-kamen čuva ime.
|
||||
tile.memorial = Spomen-kamen
|
||||
tile.memorial_info = Ovdje počiva {0} ({1}, razina {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Dvorana palih
|
||||
hall.count = Palih stvorenja u sjećanju: {0}
|
||||
hall.empty = Još nitko nije pao. Neka tako i ostane.
|
||||
hall.kills = Ubijenih junaka: {0}
|
||||
ov.fallen = Pali ({0})
|
||||
ov.living = Živi
|
||||
cause.slain = ubojica: {0}
|
||||
cause.lava = smrt u lavi
|
||||
cause.water = utapanje
|
||||
cause.snare = smrt u zamci
|
||||
cause.lightning = udar munje
|
||||
cause.fell = pogibija u boju
|
||||
@@ -264,6 +264,8 @@ val.medium = Közepes
|
||||
val.high = Magas
|
||||
val.normal = Normál
|
||||
val.large = Nagy
|
||||
val.small = Kicsi
|
||||
val.auto_plain = Automatikus
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera előre
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Feltárás
|
||||
blk.camera = Mutat
|
||||
blk.gold = Arany +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Időjárás a felszínen
|
||||
blk.talentxp = Tehetség-tapasztalat +
|
||||
blk.if = Ha
|
||||
blk.repeat = Ismétlés
|
||||
blk.trigger = Indító be/ki
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Eloszlatja a háború ködét egy zóna fölött.
|
||||
blk.help.camera = A kamerát egy zónához mozgatja.
|
||||
blk.help.gold = Aranyat ad (vagy vesz el).
|
||||
blk.help.mana = Manát ad (vagy vesz el).
|
||||
blk.help.weather = Beállítja az időjárást a felszínen (derült, eső, köd vagy vihar) ennyi másodpercre; 0 esetén megmarad, amíg egy másik blokk meg nem változtatja.
|
||||
blk.help.talentxp = Tapasztalatot ad a játékosnak a mesteri tehetségekhez.
|
||||
blk.help.if = A benne lévő blokkokat csak akkor futtatja, ha a feltétel teljesül, különben a "különben" alattiakat.
|
||||
blk.help.repeat = A benne lévő blokkokat többször lefuttatja.
|
||||
blk.help.trigger = Egy indítót bekapcsol (újra elsülhet) vagy kikapcsol.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = vagy
|
||||
blk.v.not = nem
|
||||
blk.v.bool = Igen/nem
|
||||
blk.v.eventkind = Indító egység fajtája
|
||||
blk.v.weatheris = Az időjárás a felszínen
|
||||
blk.w_at = itt:
|
||||
blk.w_by = ennyivel:
|
||||
blk.w_in = itt:
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Ismételd, amíg
|
||||
blk.dountil = Ismételd, majd ellenőrizd: amíg
|
||||
blk.wait = Várj
|
||||
blk.w_seconds = másodperc
|
||||
blk.w_for = időtartam:
|
||||
blk.help.until = A benne lévő blokkokat újra meg újra lefuttatja, amíg a feltétel nem teljesül - minden futás előtt ellenőrzi, így ha már teljesül, a belseje egyszer sem fut le.
|
||||
blk.help.dountil = Lefuttatja a benne lévő blokkokat, majd ellenőrzi a feltételt, és addig ismétel, amíg nem teljesül - a belseje legalább egyszer mindig lefut.
|
||||
blk.help.wait = Néhány másodpercre szünetelteti ezt a szkriptet, aztán a következő blokkal folytatja (ciklusokon belül is).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {1} {0}
|
||||
msg.creature_fallen = Elesett: {0}. Egy emlékkő őrzi a nevét.
|
||||
tile.memorial = Emlékkő
|
||||
tile.memorial_info = Itt nyugszik {0} ({1}, szint: {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Az elesettek csarnoka
|
||||
hall.count = {0} elesett lényre emlékezünk
|
||||
hall.empty = Még senki sem esett el. Így maradjon sokáig.
|
||||
hall.kills = Legyőzött hősök: {0}
|
||||
ov.fallen = Elesettek ({0})
|
||||
ov.living = Élők
|
||||
cause.slain = megölte: {0}
|
||||
cause.lava = lávában égett
|
||||
cause.water = vízbe fulladt
|
||||
cause.snare = csapdába lépett
|
||||
cause.lightning = villám sújtotta
|
||||
cause.fell = elesett
|
||||
@@ -264,6 +264,8 @@ val.medium = Media
|
||||
val.high = Alta
|
||||
val.normal = Normale
|
||||
val.large = Grande
|
||||
val.small = Piccolo
|
||||
val.auto_plain = Automatico
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Camera avanti
|
||||
@@ -1366,6 +1368,8 @@ blk.reveal = Rivela
|
||||
blk.camera = Mostra
|
||||
blk.gold = Oro +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Meteo in superficie
|
||||
blk.talentxp = Esperienza talenti +
|
||||
blk.if = Se
|
||||
blk.repeat = Ripeti
|
||||
blk.trigger = Commuta trigger
|
||||
@@ -1385,6 +1389,8 @@ blk.help.reveal = Solleva la nebbia di guerra su una zona.
|
||||
blk.help.camera = Sposta la telecamera su una zona.
|
||||
blk.help.gold = Dà (o toglie) oro.
|
||||
blk.help.mana = Dà (o toglie) mana.
|
||||
blk.help.weather = Imposta il meteo in superficie (sereno, pioggia, nebbia o tempesta) per tanti secondi; 0 lo mantiene finché un altro blocco non lo cambia.
|
||||
blk.help.talentxp = Dà al giocatore esperienza per i talenti del maestro.
|
||||
blk.help.if = Esegue i blocchi interni solo se la condizione è vera, altrimenti quelli sotto "altrimenti".
|
||||
blk.help.repeat = Esegue più volte i blocchi interni.
|
||||
blk.help.trigger = Attiva un trigger (può scattare di nuovo) o lo disattiva.
|
||||
@@ -1407,6 +1413,7 @@ blk.v.or = o
|
||||
blk.v.not = non
|
||||
blk.v.bool = Sì/no
|
||||
blk.v.eventkind = L'unità innesco è
|
||||
blk.v.weatheris = Il meteo in superficie è
|
||||
blk.w_at = presso
|
||||
blk.w_by = di
|
||||
blk.w_in = in
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Ripeti finché
|
||||
blk.dountil = Ripeti, poi controlla: finché
|
||||
blk.wait = Attendi
|
||||
blk.w_seconds = secondi
|
||||
blk.w_for = per
|
||||
blk.help.until = Esegue i blocchi interni più volte finché la condizione è vera; viene controllata prima di ogni giro, quindi se è già vera il contenuto non viene eseguito.
|
||||
blk.help.dountil = Esegue i blocchi interni, poi controlla la condizione e ripete finché è vera; il contenuto viene eseguito almeno una volta.
|
||||
blk.help.wait = Mette in pausa questo script per alcuni secondi, poi prosegue con il blocco successivo (anche nei cicli).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} ci ha lasciati. Una lapide commemorativa ne custodisce il nome.
|
||||
tile.memorial = Lapide commemorativa
|
||||
tile.memorial_info = Qui giace {0} ({1}, livello {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Sala dei caduti
|
||||
hall.count = Creature cadute ricordate: {0}
|
||||
hall.empty = Nessuno è ancora caduto. Che duri a lungo.
|
||||
hall.kills = Eroi uccisi: {0}
|
||||
ov.fallen = Caduti ({0})
|
||||
ov.living = Vivi
|
||||
cause.slain = morte per mano di: {0}
|
||||
cause.lava = morte nella lava
|
||||
cause.water = annegamento
|
||||
cause.snare = morte in una tagliola
|
||||
cause.lightning = colpo di fulmine
|
||||
cause.fell = caduta in battaglia
|
||||
@@ -264,6 +264,8 @@ val.medium = Middel
|
||||
val.high = Hoog
|
||||
val.normal = Normaal
|
||||
val.large = Groot
|
||||
val.small = Klein
|
||||
val.auto_plain = Automatisch
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Camera vooruit
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Onthullen
|
||||
blk.camera = Tonen
|
||||
blk.gold = Goud +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Weer bovengronds
|
||||
blk.talentxp = Talentervaring +
|
||||
blk.if = Als
|
||||
blk.repeat = Herhaal
|
||||
blk.trigger = Trigger schakelen
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Heft de oorlogsmist boven een zone op.
|
||||
blk.help.camera = Verplaatst de camera naar een zone.
|
||||
blk.help.gold = Geeft (of neemt) goud.
|
||||
blk.help.mana = Geeft (of neemt) mana.
|
||||
blk.help.weather = Zet het weer bovengronds (helder, regen, mist of storm) voor zoveel seconden; 0 houdt het tot een ander blok het verandert.
|
||||
blk.help.talentxp = Geeft de speler ervaring voor de meestertalenten.
|
||||
blk.help.if = Voert de blokken erin alleen uit als de voorwaarde klopt, anders die onder "anders".
|
||||
blk.help.repeat = Voert de blokken erin meerdere keren uit.
|
||||
blk.help.trigger = Zet een trigger aan (hij mag weer afgaan) of uit.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = of
|
||||
blk.v.not = niet
|
||||
blk.v.bool = Ja/nee
|
||||
blk.v.eventkind = Triggerunit is een
|
||||
blk.v.weatheris = Weer bovengronds is
|
||||
blk.w_at = op
|
||||
blk.w_by = met
|
||||
blk.w_in = in
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Herhaal tot
|
||||
blk.dountil = Herhaal, dan controleer: tot
|
||||
blk.wait = Wacht
|
||||
blk.w_seconds = seconden
|
||||
blk.w_for = voor
|
||||
blk.help.until = Voert de blokken erin steeds opnieuw uit tot de voorwaarde klopt - gecontroleerd voor elke ronde, dus als ze al klopt, wordt de inhoud helemaal niet uitgevoerd.
|
||||
blk.help.dountil = Voert de blokken erin uit, controleert dan de voorwaarde en herhaalt tot ze klopt - de inhoud wordt altijd minstens een keer uitgevoerd.
|
||||
blk.help.wait = Pauzeert dit script een aantal seconden en gaat dan verder met het volgende blok (ook binnen lussen).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} is gesneuveld. Een gedenksteen bewaart de naam.
|
||||
tile.memorial = Gedenksteen
|
||||
tile.memorial_info = Hier rust {0} ({1}, niveau {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Hal der gevallenen
|
||||
hall.count = Gevallen wezens in herinnering: {0}
|
||||
hall.empty = Nog niemand is gevallen. Moge het zo blijven.
|
||||
hall.kills = Verslagen helden: {0}
|
||||
ov.fallen = Gevallen ({0})
|
||||
ov.living = Levend
|
||||
cause.slain = verslagen door: {0}
|
||||
cause.lava = verbrand in lava
|
||||
cause.water = verdronken
|
||||
cause.snare = in een klem getrapt
|
||||
cause.lightning = door de bliksem getroffen
|
||||
cause.fell = gesneuveld
|
||||
@@ -264,6 +264,8 @@ val.medium = Middels
|
||||
val.high = Høy
|
||||
val.normal = Normal
|
||||
val.large = Stor
|
||||
val.small = Liten
|
||||
val.auto_plain = Automatisk
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera fram
|
||||
@@ -1373,6 +1375,8 @@ blk.reveal = Avslør
|
||||
blk.camera = Vis
|
||||
blk.gold = Gull +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Været over jorden
|
||||
blk.talentxp = Talenterfaring +
|
||||
blk.if = Hvis
|
||||
blk.repeat = Gjenta
|
||||
blk.trigger = Bytt utløser
|
||||
@@ -1392,6 +1396,8 @@ blk.help.reveal = Løfter krigens tåke over en sone.
|
||||
blk.help.camera = Flytter kameraet til en sone.
|
||||
blk.help.gold = Gir (eller tar) gull.
|
||||
blk.help.mana = Gir (eller tar) mana.
|
||||
blk.help.weather = Setter været over jorden (klart, regn, tåke eller storm) i så mange sekunder; 0 beholder det til en annen blokk endrer det.
|
||||
blk.help.talentxp = Gir spilleren erfaring til mestertalentene.
|
||||
blk.help.if = Kjører blokkene inni bare hvis betingelsen gjelder, ellers de under "ellers".
|
||||
blk.help.repeat = Kjører blokkene inni flere ganger.
|
||||
blk.help.trigger = Slår en utløser på (den kan utløses igjen) eller av.
|
||||
@@ -1414,6 +1420,7 @@ blk.v.or = eller
|
||||
blk.v.not = ikke
|
||||
blk.v.bool = Ja/nei
|
||||
blk.v.eventkind = Utløserenhet er en
|
||||
blk.v.weatheris = Været over jorden er
|
||||
blk.w_at = ved
|
||||
blk.w_by = med
|
||||
blk.w_in = i
|
||||
@@ -1462,6 +1469,7 @@ blk.until = Gjenta til
|
||||
blk.dountil = Gjenta, sjekk så: til
|
||||
blk.wait = Vent
|
||||
blk.w_seconds = sekunder
|
||||
blk.w_for = i
|
||||
blk.help.until = Kjører blokkene inni igjen og igjen til betingelsen gjelder - sjekkes før hver kjøring, så hvis den allerede gjelder, kjører innholdet ikke i det hele tatt.
|
||||
blk.help.dountil = Kjører blokkene inni, sjekker så betingelsen og gjentar til den gjelder - innholdet kjører alltid minst én gang.
|
||||
blk.help.wait = Setter dette skriptet på pause i noen sekunder, så fortsetter det med neste blokk (også inni løkker).
|
||||
@@ -1745,3 +1753,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} har falt. En minnestein bevarer navnet.
|
||||
tile.memorial = Minnestein
|
||||
tile.memorial_info = Her hviler {0} ({1}, nivå {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = De falnes hall
|
||||
hall.count = Falne skapninger i minnet: {0}
|
||||
hall.empty = Ingen har falt ennå. Lenge må det vare.
|
||||
hall.kills = Drepte helter: {0}
|
||||
ov.fallen = Falne ({0})
|
||||
ov.living = Levende
|
||||
cause.slain = drept av: {0}
|
||||
cause.lava = brent i lava
|
||||
cause.water = druknet
|
||||
cause.snare = tråkket i en saks
|
||||
cause.lightning = truffet av lynet
|
||||
cause.fell = falt
|
||||
@@ -264,6 +264,8 @@ val.medium = Médio
|
||||
val.high = Alto
|
||||
val.normal = Normal
|
||||
val.large = Grande
|
||||
val.small = Pequeno
|
||||
val.auto_plain = Automático
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Câmara p/ frente
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Revelar
|
||||
blk.camera = Mostrar
|
||||
blk.gold = Ouro +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Tempo na superfície
|
||||
blk.talentxp = Experiência de talentos +
|
||||
blk.if = Se
|
||||
blk.repeat = Repetir
|
||||
blk.trigger = Ligar gatilho
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Levanta o nevoeiro de guerra sobre uma zona.
|
||||
blk.help.camera = Move a câmara para uma zona.
|
||||
blk.help.gold = Dá (ou tira) ouro.
|
||||
blk.help.mana = Dá (ou tira) mana.
|
||||
blk.help.weather = Define o tempo na superfície (limpo, chuva, nevoeiro ou temporal) durante tantos segundos; 0 mantém-no até outro bloco o mudar.
|
||||
blk.help.talentxp = Dá ao jogador experiência para os talentos do mestre.
|
||||
blk.help.if = Executa os blocos de dentro só se a condição se verificar; caso contrário, os de "senão".
|
||||
blk.help.repeat = Executa os blocos de dentro várias vezes.
|
||||
blk.help.trigger = Liga um gatilho (pode disparar de novo) ou desliga-o.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = ou
|
||||
blk.v.not = não
|
||||
blk.v.bool = Sim/não
|
||||
blk.v.eventkind = Unidade do gatilho é
|
||||
blk.v.weatheris = Tempo na superfície é
|
||||
blk.w_at = em
|
||||
blk.w_by = por
|
||||
blk.w_in = em
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Repetir até
|
||||
blk.dountil = Repetir, depois verificar: até
|
||||
blk.wait = Esperar
|
||||
blk.w_seconds = segundos
|
||||
blk.w_for = durante
|
||||
blk.help.until = Executa os blocos de dentro uma e outra vez até a condição se verificar - verificada antes de cada execução, por isso, se já se verificar, o interior nem chega a correr.
|
||||
blk.help.dountil = Executa os blocos de dentro, depois verifica a condição e repete até ela se verificar - o interior corre sempre pelo menos uma vez.
|
||||
blk.help.wait = Pausa este script durante alguns segundos e depois continua com o bloco seguinte (também dentro de ciclos).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} tombou. Uma pedra memorial guarda o seu nome.
|
||||
tile.memorial = Pedra memorial
|
||||
tile.memorial_info = Aqui jaz {0} ({1}, nível {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Salão dos caídos
|
||||
hall.count = Criaturas caídas recordadas: {0}
|
||||
hall.empty = Ainda ninguém caiu. Que assim continue.
|
||||
hall.kills = Heróis abatidos: {0}
|
||||
ov.fallen = Caídos ({0})
|
||||
ov.living = Vivos
|
||||
cause.slain = morte às mãos de: {0}
|
||||
cause.lava = morte na lava
|
||||
cause.water = afogamento
|
||||
cause.snare = morte numa armadilha
|
||||
cause.lightning = morte por raio
|
||||
cause.fell = queda em combate
|
||||
@@ -264,6 +264,8 @@ val.medium = Mediu
|
||||
val.high = Înalt
|
||||
val.normal = Normal
|
||||
val.large = Mare
|
||||
val.small = Mic
|
||||
val.auto_plain = Automat
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Cameră înainte
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Dezvăluie
|
||||
blk.camera = Arată
|
||||
blk.gold = Aur +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Vremea la suprafață
|
||||
blk.talentxp = Experiență pentru talente +
|
||||
blk.if = Dacă
|
||||
blk.repeat = Repetă
|
||||
blk.trigger = Comută declanșator
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Ridică ceața războiului de pe o zonă.
|
||||
blk.help.camera = Mută camera la o zonă.
|
||||
blk.help.gold = Dă (sau ia) aur.
|
||||
blk.help.mana = Dă (sau ia) mana.
|
||||
blk.help.weather = Setează vremea la suprafață (senin, ploaie, ceață sau furtună) pentru atâtea secunde; 0 o păstrează până când alt bloc o schimbă.
|
||||
blk.help.talentxp = Dă jucătorului experiență pentru talentele stăpânului.
|
||||
blk.help.if = Rulează blocurile dinăuntru doar dacă condiția e adevărată, altfel pe cele de sub "altfel".
|
||||
blk.help.repeat = Rulează blocurile dinăuntru de mai multe ori.
|
||||
blk.help.trigger = Pornește un declanșator (poate acționa din nou) sau îl oprește.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = sau
|
||||
blk.v.not = nu
|
||||
blk.v.bool = Da/nu
|
||||
blk.v.eventkind = Unitatea declanșatoare e un
|
||||
blk.v.weatheris = Vremea la suprafață e
|
||||
blk.w_at = la
|
||||
blk.w_by = cu
|
||||
blk.w_in = în
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Repetă până când
|
||||
blk.dountil = Repetă, apoi verifică: până când
|
||||
blk.wait = Așteaptă
|
||||
blk.w_seconds = secunde
|
||||
blk.w_for = pentru
|
||||
blk.help.until = Rulează blocurile dinăuntru iar și iar până când condiția e adevărată - verificată înaintea fiecărei rulări, deci dacă e deja adevărată, interiorul nu rulează deloc.
|
||||
blk.help.dountil = Rulează blocurile dinăuntru, apoi verifică condiția și repetă până e adevărată - interiorul rulează mereu cel puțin o dată.
|
||||
blk.help.wait = Oprește acest script câteva secunde, apoi continuă cu blocul următor (și în bucle).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} a căzut. O piatră comemorativă păstrează numele.
|
||||
tile.memorial = Piatră comemorativă
|
||||
tile.memorial_info = Aici se odihnește {0} ({1}, nivelul {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Sala celor căzuți
|
||||
hall.count = Creaturi căzute amintite: {0}
|
||||
hall.empty = Încă n-a căzut nimeni. Să rămână așa.
|
||||
hall.kills = Eroi uciși: {0}
|
||||
ov.fallen = Căzuți ({0})
|
||||
ov.living = Vii
|
||||
cause.slain = ucigaș: {0}
|
||||
cause.lava = moarte în lavă
|
||||
cause.water = înec
|
||||
cause.snare = moarte într-o capcană
|
||||
cause.lightning = lovitură de fulger
|
||||
cause.fell = moarte în luptă
|
||||
@@ -264,6 +264,8 @@ val.medium = Средне
|
||||
val.high = Высоко
|
||||
val.normal = Обычный
|
||||
val.large = Большой
|
||||
val.small = Маленький
|
||||
val.auto_plain = Авто
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Камера вперёд
|
||||
@@ -1366,6 +1368,8 @@ blk.reveal = Открыть
|
||||
blk.camera = Показать
|
||||
blk.gold = Золото +
|
||||
blk.mana = Мана +
|
||||
blk.weather = Погода на поверхности
|
||||
blk.talentxp = Опыт талантов +
|
||||
blk.if = Если
|
||||
blk.repeat = Повторить
|
||||
blk.trigger = Переключить триггер
|
||||
@@ -1385,6 +1389,8 @@ blk.help.reveal = Снимает туман войны с зоны.
|
||||
blk.help.camera = Переносит камеру к зоне.
|
||||
blk.help.gold = Даёт (или забирает) золото.
|
||||
blk.help.mana = Даёт (или забирает) ману.
|
||||
blk.help.weather = Задаёт погоду на поверхности (ясно, дождь, туман или гроза) на столько секунд; 0 оставляет её, пока другой блок её не изменит.
|
||||
blk.help.talentxp = Даёт игроку опыт для талантов мастера.
|
||||
blk.help.if = Выполняет вложенные блоки, только если условие верно, иначе - блоки под «иначе».
|
||||
blk.help.repeat = Выполняет вложенные блоки несколько раз.
|
||||
blk.help.trigger = Включает триггер (он может сработать снова) или выключает его.
|
||||
@@ -1407,6 +1413,7 @@ blk.v.or = или
|
||||
blk.v.not = не
|
||||
blk.v.bool = Да/нет
|
||||
blk.v.eventkind = Боец триггера:
|
||||
blk.v.weatheris = Погода на поверхности:
|
||||
blk.w_at = где:
|
||||
blk.w_by = на
|
||||
blk.w_in = в
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Повторять, пока не
|
||||
blk.dountil = Повторять, затем проверить: пока не
|
||||
blk.wait = Ждать
|
||||
blk.w_seconds = сек.
|
||||
blk.w_for = на
|
||||
blk.help.until = Снова и снова выполняет блоки внутри, пока условие не станет верным; проверка перед каждым проходом, поэтому если оно уже верно, блоки не выполняются.
|
||||
blk.help.dountil = Выполняет блоки внутри, затем проверяет условие и повторяет, пока оно не станет верным; блоки выполняются хотя бы один раз.
|
||||
blk.help.wait = Приостанавливает этот скрипт на несколько секунд, затем выполняется следующий блок (и внутри циклов).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Пал твой боец: {0}. Памятный камень хранит имя.
|
||||
tile.memorial = Памятный камень
|
||||
tile.memorial_info = Здесь покоится {0} ({1}, уровень {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Зал павших
|
||||
hall.count = Павших существ в памяти: {0}
|
||||
hall.empty = Пока никто не пал. Пусть так и будет.
|
||||
hall.kills = Убито героев: {0}
|
||||
ov.fallen = Павшие ({0})
|
||||
ov.living = Живые
|
||||
cause.slain = убийца: {0}
|
||||
cause.lava = гибель в лаве
|
||||
cause.water = утопление
|
||||
cause.snare = гибель в капкане
|
||||
cause.lightning = удар молнии
|
||||
cause.fell = гибель в бою
|
||||
@@ -264,6 +264,8 @@ val.medium = Stredná
|
||||
val.high = Vysoká
|
||||
val.normal = Normálna
|
||||
val.large = Veľká
|
||||
val.small = Malá
|
||||
val.auto_plain = Automaticky
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera dopredu
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Odhaliť
|
||||
blk.camera = Ukázať
|
||||
blk.gold = Zlato +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Počasie na povrchu
|
||||
blk.talentxp = Skúsenosti talentov +
|
||||
blk.if = Ak
|
||||
blk.repeat = Opakovať
|
||||
blk.trigger = Prepnúť spúšťač
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Odhalí hmlu vojny nad zónou.
|
||||
blk.help.camera = Presunie kameru na zónu.
|
||||
blk.help.gold = Dá (alebo vezme) zlato.
|
||||
blk.help.mana = Dá (alebo vezme) manu.
|
||||
blk.help.weather = Nastaví počasie na povrchu (jasno, dážď, hmla alebo búrka) na toľko sekúnd; 0 ho ponechá, kým ho iný blok nezmení.
|
||||
blk.help.talentxp = Dá hráčovi skúsenosti pre pánove talenty.
|
||||
blk.help.if = Spustí vnútorné bloky, len ak platí podmienka, inak tie pod "inak".
|
||||
blk.help.repeat = Spustí vnútorné bloky niekoľkokrát.
|
||||
blk.help.trigger = Zapne spúšťač (môže sa spustiť znova) alebo ho vypne.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = alebo
|
||||
blk.v.not = nie
|
||||
blk.v.bool = Áno/nie
|
||||
blk.v.eventkind = Spúšťacia jednotka je
|
||||
blk.v.weatheris = Počasie na povrchu je
|
||||
blk.w_at = na
|
||||
blk.w_by = o
|
||||
blk.w_in = v
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Opakovať, kým
|
||||
blk.dountil = Opakovať, potom overiť: kým
|
||||
blk.wait = Čakať
|
||||
blk.w_seconds = sekúnd
|
||||
blk.w_for = na
|
||||
blk.help.until = Spúšťa vnútorné bloky znova a znova, kým neplatí podmienka - overuje sa pred každým behom, takže ak už platí, vnútro sa nespustí vôbec.
|
||||
blk.help.dountil = Spustí vnútorné bloky, potom overí podmienku a opakuje, kým neplatí - vnútro prebehne vždy aspoň raz.
|
||||
blk.help.wait = Pozastaví tento skript na niekoľko sekúnd, potom pokračuje ďalším blokom (aj vnútri slučiek).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Padol tvoj tvor: {0}. Pamätný kameň uchová meno.
|
||||
tile.memorial = Pamätný kameň
|
||||
tile.memorial_info = Tu odpočíva {0} ({1}, úroveň {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Sieň padlých
|
||||
hall.count = Padlé tvory v pamäti: {0}
|
||||
hall.empty = Zatiaľ nikto nepadol. Nech to tak vydrží.
|
||||
hall.kills = Zabití hrdinovia: {0}
|
||||
ov.fallen = Padlí ({0})
|
||||
ov.living = Živí
|
||||
cause.slain = vrah: {0}
|
||||
cause.lava = smrť v láve
|
||||
cause.water = utopenie
|
||||
cause.snare = smrť v pasci
|
||||
cause.lightning = zásah bleskom
|
||||
cause.fell = smrť v boji
|
||||
@@ -264,6 +264,8 @@ val.medium = Средње
|
||||
val.high = Високо
|
||||
val.normal = Нормално
|
||||
val.large = Велико
|
||||
val.small = Мало
|
||||
val.auto_plain = Аутоматски
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Камера напред
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Откриј
|
||||
blk.camera = Покажи
|
||||
blk.gold = Злато +
|
||||
blk.mana = Мана +
|
||||
blk.weather = Време на површини
|
||||
blk.talentxp = Искуство талената +
|
||||
blk.if = Ако
|
||||
blk.repeat = Понови
|
||||
blk.trigger = Пребаци окидач
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Подиже маглу рата над зоном.
|
||||
blk.help.camera = Помера камеру на зону.
|
||||
blk.help.gold = Даје (или одузима) злато.
|
||||
blk.help.mana = Даје (или одузима) ману.
|
||||
blk.help.weather = Поставља време на површини (ведро, киша, магла или олуја) на толико секунди; 0 га задржава док га други блок не промени.
|
||||
blk.help.talentxp = Даје играчу искуство за господареве таленте.
|
||||
blk.help.if = Покреће унутрашње блокове само ако услов важи, иначе оне под «иначе».
|
||||
blk.help.repeat = Покреће унутрашње блокове неколико пута.
|
||||
blk.help.trigger = Укључује окидач (може поново да се активира) или га искључује.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = или
|
||||
blk.v.not = не
|
||||
blk.v.bool = Да/не
|
||||
blk.v.eventkind = Јединица окидача је
|
||||
blk.v.weatheris = Време на површини је
|
||||
blk.w_at = на
|
||||
blk.w_by = за
|
||||
blk.w_in = у
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Понављај док
|
||||
blk.dountil = Понови, затим провери: док
|
||||
blk.wait = Чекај
|
||||
blk.w_seconds = секунди
|
||||
blk.w_for = на
|
||||
blk.help.until = Покреће унутрашње блокове изнова и изнова док услов не почне да важи - проверава се пре сваког пролаза, па ако већ важи, унутрашњост се уопште не покреће.
|
||||
blk.help.dountil = Покреће унутрашње блокове, затим проверава услов и понавља док не почне да важи - унутрашњост се увек покрене бар једном.
|
||||
blk.help.wait = Паузира ову скрипту неколико секунди, а затим наставља следећим блоком (и унутар петљи).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = Пало је твоје створење: {0}. Спомен-камен чува име.
|
||||
tile.memorial = Спомен-камен
|
||||
tile.memorial_info = Овде почива {0} ({1}, ниво {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = Дворана палих
|
||||
hall.count = Палих створења у сећању: {0}
|
||||
hall.empty = Још нико није пао. Нека тако и остане.
|
||||
hall.kills = Убијених хероја: {0}
|
||||
ov.fallen = Пали ({0})
|
||||
ov.living = Живи
|
||||
cause.slain = убица: {0}
|
||||
cause.lava = смрт у лави
|
||||
cause.water = утапање
|
||||
cause.snare = смрт у замци
|
||||
cause.lightning = удар муње
|
||||
cause.fell = погибија у боју
|
||||
@@ -264,6 +264,8 @@ val.medium = Medel
|
||||
val.high = Hög
|
||||
val.normal = Normal
|
||||
val.large = Stor
|
||||
val.small = Liten
|
||||
val.auto_plain = Automatisk
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Kamera framåt
|
||||
@@ -1374,6 +1376,8 @@ blk.reveal = Avslöja
|
||||
blk.camera = Visa
|
||||
blk.gold = Guld +
|
||||
blk.mana = Mana +
|
||||
blk.weather = Vädret ovan jord
|
||||
blk.talentxp = Talangerfarenhet +
|
||||
blk.if = Om
|
||||
blk.repeat = Upprepa
|
||||
blk.trigger = Växla trigger
|
||||
@@ -1393,6 +1397,8 @@ blk.help.reveal = Lyfter krigsdimman över en zon.
|
||||
blk.help.camera = Flyttar kameran till en zon.
|
||||
blk.help.gold = Ger (eller tar) guld.
|
||||
blk.help.mana = Ger (eller tar) mana.
|
||||
blk.help.weather = Sätter vädret ovan jord (klart, regn, dimma eller storm) i så många sekunder; 0 behåller det tills ett annat block ändrar det.
|
||||
blk.help.talentxp = Ger spelaren erfarenhet till mästartalangerna.
|
||||
blk.help.if = Kör blocken inuti bara om villkoret gäller, annars de under "annars".
|
||||
blk.help.repeat = Kör blocken inuti flera gånger.
|
||||
blk.help.trigger = Slår på en trigger (den får utlösas igen) eller av.
|
||||
@@ -1415,6 +1421,7 @@ blk.v.or = eller
|
||||
blk.v.not = inte
|
||||
blk.v.bool = Ja/nej
|
||||
blk.v.eventkind = Triggerenheten är en
|
||||
blk.v.weatheris = Vädret ovan jord är
|
||||
blk.w_at = vid
|
||||
blk.w_by = med
|
||||
blk.w_in = i
|
||||
@@ -1463,6 +1470,7 @@ blk.until = Upprepa tills
|
||||
blk.dountil = Upprepa, kolla sedan: tills
|
||||
blk.wait = Vänta
|
||||
blk.w_seconds = sekunder
|
||||
blk.w_for = i
|
||||
blk.help.until = Kör blocken inuti om och om igen tills villkoret gäller - kontrolleras före varje varv, så om det redan gäller körs insidan inte alls.
|
||||
blk.help.dountil = Kör blocken inuti, kontrollerar sedan villkoret och upprepar tills det gäller - insidan körs alltid minst en gång.
|
||||
blk.help.wait = Pausar det här skriptet några sekunder, sedan fortsätter det med nästa block (även inuti loopar).
|
||||
@@ -1746,3 +1754,17 @@ name.epithet = {0} {1}
|
||||
msg.creature_fallen = {0} har fallit. En minnessten bevarar namnet.
|
||||
tile.memorial = Minnessten
|
||||
tile.memorial_info = Här vilar {0} ({1}, nivå {2}).
|
||||
|
||||
# hall of the fallen
|
||||
hall.title = De stupades sal
|
||||
hall.count = Stupade varelser i minnet: {0}
|
||||
hall.empty = Ingen har stupat än. Må det länge förbli så.
|
||||
hall.kills = Dräpta hjältar: {0}
|
||||
ov.fallen = Stupade ({0})
|
||||
ov.living = Levande
|
||||
cause.slain = dräpt av: {0}
|
||||
cause.lava = brann i lava
|
||||
cause.water = drunknade
|
||||
cause.snare = klev i en fälla
|
||||
cause.lightning = träffad av blixten
|
||||
cause.fell = stupade
|
||||
@@ -4,6 +4,13 @@ server {
|
||||
index index.html;
|
||||
# test build: always reload fresh files
|
||||
add_header Cache-Control "no-cache";
|
||||
# compressed downloads: the big files are packed once at build time (*.gz, see Dockerfile),
|
||||
# the rest on the fly (the wasm goes from about 8.8 MB to 2.6 MB)
|
||||
gzip_static on;
|
||||
gzip on;
|
||||
gzip_types application/wasm application/javascript text/css application/json application/manifest+json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
gzip_vary on;
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
+28
-1
@@ -63,8 +63,13 @@ pub struct Profile {
|
||||
pub totals: BTreeMap<String, f64>,
|
||||
#[serde(default)]
|
||||
pub unlocked: BTreeMap<String, u64>, // achievement id ("heroes.2", "flawless") -> unix time
|
||||
#[serde(default)]
|
||||
pub hall: Vec<crate::names::Fallen>, // the hall of the fallen over all games (newest last)
|
||||
}
|
||||
|
||||
// the hall of the fallen keeps this many
|
||||
pub const HALL_MAX: usize = 200;
|
||||
|
||||
impl Profile {
|
||||
pub fn total(&self, id: &str) -> f64 {
|
||||
self.totals.get(id).copied().unwrap_or(0.0)
|
||||
@@ -80,6 +85,15 @@ impl Profile {
|
||||
let e = self.unlocked.entry(k.clone()).or_insert(*t);
|
||||
*e = (*e).min(*t);
|
||||
}
|
||||
// the fallen of both, once each, in the order they fell
|
||||
for f in &other.hall {
|
||||
if !self.hall.iter().any(|h| h.name == f.name && h.kind == f.kind && h.when == f.when) {
|
||||
self.hall.push(f.clone());
|
||||
}
|
||||
}
|
||||
self.hall.sort_by_key(|h| h.when);
|
||||
let extra = self.hall.len().saturating_sub(HALL_MAX);
|
||||
self.hall.drain(..extra);
|
||||
}
|
||||
|
||||
// unlocks what the totals have reached; returns the new ones
|
||||
@@ -117,6 +131,7 @@ pub struct Context {
|
||||
pub struct Tracker {
|
||||
pub profile: Profile,
|
||||
flushed: Vec<f32>,
|
||||
hall_seen: usize, // entries of the game's hall already in the profile
|
||||
last_time: f32,
|
||||
ended: bool,
|
||||
t: f32,
|
||||
@@ -126,12 +141,13 @@ pub struct Tracker {
|
||||
|
||||
impl Tracker {
|
||||
pub fn new(profile: Profile) -> Tracker {
|
||||
Tracker { profile, flushed: vec![0.0; k::COUNTERS], last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
|
||||
Tracker { profile, flushed: vec![0.0; k::COUNTERS], hall_seen: 0, last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
|
||||
}
|
||||
|
||||
// a different game started (new, loaded): nothing of it has been counted yet
|
||||
fn restart(&mut self, g: &Game) {
|
||||
self.flushed = (0..k::COUNTERS).map(|i| g.stats.get(i)).collect();
|
||||
self.hall_seen = g.hall.len();
|
||||
self.ended = g.state != 0;
|
||||
}
|
||||
|
||||
@@ -154,6 +170,17 @@ impl Tracker {
|
||||
let played = self.t;
|
||||
self.t = 0.0;
|
||||
let mut new = Vec::new();
|
||||
// the newly fallen go into the profile's hall
|
||||
if g.hall.len() > self.hall_seen {
|
||||
for f in &g.hall[self.hall_seen..] {
|
||||
self.profile.hall.push(crate::names::Fallen { when: now, ..f.clone() });
|
||||
}
|
||||
self.hall_seen = g.hall.len();
|
||||
let extra = self.profile.hall.len().saturating_sub(HALL_MAX);
|
||||
self.profile.hall.drain(..extra);
|
||||
self.dirty = true;
|
||||
self.upload = true;
|
||||
}
|
||||
// the counters of the game
|
||||
for i in 0..k::COUNTERS {
|
||||
let cur = g.stats.get(i);
|
||||
|
||||
@@ -14,8 +14,9 @@ const THINK: [f32; 3] = [5.2, 3.25, 1.95]; // seconds between two thoughts (a ca
|
||||
const MAX_RADIUS: [i32; 3] = [11, 15, 19]; // how far the dungeon grows around the heart
|
||||
const MAX_IMPS: [usize; 3] = [5, 7, 9];
|
||||
const FIRST_ATTACK: [f32; 3] = [940.0, 625.0, 430.0]; // no attack before (seconds)
|
||||
const BOLDNESS: [f32; 3] = [1.5, 1.15, 0.9]; // own army / enemy army needed to attack
|
||||
const BOLDNESS: [f32; 3] = [1.0, 1.2, 1.35]; // own army / enemy army needed to attack (easy is reckless, hard waits)
|
||||
const MARK_REACH: i32 = 5; // an AI master marks only this close to its own ground
|
||||
const CREATURE_TARGET: [usize; 3] = [24, 50, 80]; // beds an AI master builds up to
|
||||
const MAX_TOWERS: [usize; 3] = [1, 2, 4];
|
||||
const START_GOLD: [f32; 3] = [1500.0, 2500.0, 3500.0];
|
||||
|
||||
@@ -51,10 +52,11 @@ impl Game {
|
||||
self.ai_imps(d, cx, cy);
|
||||
self.ai_dig(k, d, cx, cy);
|
||||
self.ai_gold(cx, cy);
|
||||
self.ai_rooms(cx, cy);
|
||||
self.ai_rooms(d, cx, cy);
|
||||
self.ai_towers(d, cx, cy);
|
||||
self.ai_fight(k, d, side, cx, cy);
|
||||
self.ai_talents();
|
||||
let focus = self.rivals[k].focus;
|
||||
self.ai_talents(focus);
|
||||
}
|
||||
|
||||
// the tiles an AI master may mark: within MARK_REACH tiles of its own ground
|
||||
@@ -161,18 +163,23 @@ impl Game {
|
||||
|
||||
// gold running low: a tunnel to the nearest vein that nobody works on yet
|
||||
fn ai_gold(&mut self, cx: i32, cy: i32) {
|
||||
if self.gold > 1500.0 {
|
||||
// gold for one and a half paydays and a little more: enough
|
||||
if self.gold > self.next_payday_cost() * 1.5 + 1000.0 {
|
||||
return;
|
||||
}
|
||||
let me = self.me;
|
||||
// a marked vein the imps can reach (next to own ground) is being worked already
|
||||
let busy = (0..self.map.t.len()).any(|i| {
|
||||
// marked veins the imps can reach (next to own ground) are being worked already; one vein
|
||||
// for every three imps at a time
|
||||
let side = me.side().min(7);
|
||||
let busy = (0..self.map.t.len())
|
||||
.filter(|&i| {
|
||||
let t = &self.map.t[i];
|
||||
let (x, y) = xy(i);
|
||||
t.marked && t.vein().is_some() && DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == me && n.walkable()))
|
||||
});
|
||||
if busy {
|
||||
return; // a vein is being worked already
|
||||
t.marked && t.mark_by == side && t.vein().is_some() && DIR4.iter().any(|&(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |n| n.owner == me && n.walkable()))
|
||||
})
|
||||
.count();
|
||||
if busy >= (self.worker_count() / 3).max(1) {
|
||||
return;
|
||||
}
|
||||
self.ai_mark_tunnel(cx, cy, |g: &Game, i: usize| {
|
||||
let t = &g.map.t[i];
|
||||
@@ -220,7 +227,7 @@ impl Game {
|
||||
}
|
||||
|
||||
// the next room of the build order, where there is free own floor for it
|
||||
fn ai_rooms(&mut self, cx: i32, cy: i32) {
|
||||
fn ai_rooms(&mut self, d: usize, cx: i32, cy: i32) {
|
||||
for &(room, w, h, n) in BUILD.iter() {
|
||||
// only what this master has researched (the same rules as the player)
|
||||
if !self.ai_knows(crate::ui::Tool::Room(room)) {
|
||||
@@ -239,6 +246,29 @@ impl Game {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// the base is done: a bigger treasury when it is nearly full
|
||||
if self.gold > self.gold_cap() * 0.85 {
|
||||
let cost = ROOM_COST[0] as f32 * 9.0;
|
||||
if self.gold >= cost + 400.0 {
|
||||
if let Some((x0, y0)) = self.ai_free_rect(cx, cy, 3, 3) {
|
||||
self.build_rect(0, x0, y0, x0 + 2, y0 + 2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// more beds and food while the army may still grow (by difficulty)
|
||||
if self.counts[1] >= CREATURE_TARGET[d] || self.free_beds() > 2 {
|
||||
return;
|
||||
}
|
||||
let room = if self.counts[2] * 10 < self.counts[1] * 6 { 2 } else { 1 };
|
||||
let (w, h) = (4, 3);
|
||||
let cost = ROOM_COST[room] as f32 * (w * h) as f32;
|
||||
if self.gold < cost + 400.0 {
|
||||
return;
|
||||
}
|
||||
if let Some((x0, y0)) = self.ai_free_rect(cx, cy, w, h) {
|
||||
self.build_rect(room, x0, y0, x0 + w - 1, y0 + h - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// has this master researched the tool (rooms, towers)?
|
||||
@@ -295,6 +325,11 @@ impl Game {
|
||||
self.place_mark(cx + 1, cy + 3, false);
|
||||
}
|
||||
let army = self.side_army(side);
|
||||
// heroes or enemy units in the dungeon: everybody goes for them (the rally flag) and the
|
||||
// master casts lightning on them and heals his hurt creatures
|
||||
if !self.is_partner(side) && self.ai_defend(k) {
|
||||
return;
|
||||
}
|
||||
// co-op: go where the allies go
|
||||
if self.is_partner(side) {
|
||||
let lead = self.master_sides().into_iter().find(|&s| s != side && !self.is_out(s) && self.allied(side, s));
|
||||
@@ -363,6 +398,49 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
// the defence of the dungeon; true while there are intruders
|
||||
fn ai_defend(&mut self, k: usize) -> bool {
|
||||
let me = self.me;
|
||||
let intruders: Vec<(f32, f32)> = self
|
||||
.ents
|
||||
.iter()
|
||||
.filter(|e| e.alive && !e.held && e.captive == 0 && self.is_hostile(e) && self.map.get(e.x as i32, e.y as i32).map_or(false, |t| t.owner == me))
|
||||
.map(|e| (e.x, e.y))
|
||||
.collect();
|
||||
if intruders.is_empty() {
|
||||
if self.rivals[k].defending {
|
||||
self.rivals[k].defending = false;
|
||||
self.flag = None;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// the middle of the intruders, on walkable ground
|
||||
let n = intruders.len() as f32;
|
||||
let (mx, my) = intruders.iter().fold((0.0, 0.0), |a, p| (a.0 + p.0 / n, a.1 + p.1 / n));
|
||||
let near = intruders.iter().min_by(|a, b| ((a.0 - mx).powi(2) + (a.1 - my).powi(2)).partial_cmp(&((b.0 - mx).powi(2) + (b.1 - my).powi(2))).unwrap()).copied().unwrap();
|
||||
let tile = idx(near.0 as i32, near.1 as i32);
|
||||
if self.rivals[k].attacking {
|
||||
self.rivals[k].attacking = false; // home first
|
||||
}
|
||||
self.rivals[k].defending = true;
|
||||
if self.map.t[tile].walkable() && self.flag != Some(tile) {
|
||||
self.flag = Some(tile);
|
||||
}
|
||||
// lightning on the thickest of them, healing for the hurt
|
||||
let bolt = 50.0 * self.talents.spell_cost();
|
||||
if self.mana >= bolt + 40.0 && self.known(crate::game::LIGHTNING_TOOL) {
|
||||
self.lightning(near.0, near.1);
|
||||
}
|
||||
let heal = 60.0 * self.talents.spell_cost();
|
||||
let hurt = self.ents.iter().filter(|e| e.alive && e.creature() && e.hp < e.max_hp * 0.5 && (e.x - near.0).powi(2) + (e.y - near.1).powi(2) < 36.0).map(|e| (e.x, e.y)).next();
|
||||
if let Some((hx, hy)) = hurt {
|
||||
if self.mana >= heal + 40.0 && self.known(crate::game::HEAL_TOOL) {
|
||||
self.heal(hx, hy);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// is an enemy heart near tile f?
|
||||
fn ai_enemy_heart_near(&self, side: u8, f: usize) -> bool {
|
||||
let (fx, fy) = xy(f);
|
||||
|
||||
+105
-1
@@ -1,6 +1,7 @@
|
||||
// Audio engine: music (XM players with crossfades) and synthesised sound effects.
|
||||
// The browser pulls samples through dc_audio_render() from our JS plugin (www/dc_plugin.js),
|
||||
// which starts the AudioContext on the first user interaction.
|
||||
// which starts the AudioContext on the first user interaction. Desktop builds play through
|
||||
// the system's sound output (module `native` below).
|
||||
|
||||
use crate::sfx;
|
||||
use crate::tracks::TRACKS;
|
||||
@@ -244,6 +245,109 @@ pub extern "C" fn dc_audio_render(frames: u32) -> *const f32 {
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ desktop sound output
|
||||
|
||||
// The game renders a little ahead into a queue each frame; the sound card's thread takes from it.
|
||||
// The engine itself stays on the game thread, like in the browser.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod native {
|
||||
use super::STATE;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
// seconds rendered ahead: covers a slow frame or two
|
||||
const AHEAD: f32 = 0.07;
|
||||
|
||||
struct Out {
|
||||
_stream: cpal::Stream,
|
||||
queue: Arc<Mutex<VecDeque<f32>>>, // interleaved stereo
|
||||
rate: f32,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static OUT: RefCell<Option<Out>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
// opens the default output device and starts the engine; without one the game stays silent
|
||||
pub fn start() {
|
||||
match open() {
|
||||
Ok(out) => {
|
||||
super::dc_audio_init(out.rate);
|
||||
OUT.with(|o| *o.borrow_mut() = Some(out));
|
||||
}
|
||||
Err(e) => eprintln!("DungeonCraft: no sound ({})", e),
|
||||
}
|
||||
}
|
||||
|
||||
fn open() -> Result<Out, String> {
|
||||
let dev = cpal::default_host().default_output_device().ok_or("no output device")?;
|
||||
let cfg = dev.default_output_config().map_err(|e| e.to_string())?;
|
||||
let rate = cfg.sample_rate() as f32;
|
||||
let channels = cfg.channels() as usize;
|
||||
let queue = Arc::new(Mutex::new(VecDeque::new()));
|
||||
let sc: cpal::StreamConfig = cfg.config();
|
||||
use cpal::SampleFormat as F;
|
||||
let stream = match cfg.sample_format() {
|
||||
F::F32 => build::<f32>(&dev, &sc, channels, queue.clone()),
|
||||
F::I16 => build::<i16>(&dev, &sc, channels, queue.clone()),
|
||||
F::U16 => build::<u16>(&dev, &sc, channels, queue.clone()),
|
||||
F::I32 => build::<i32>(&dev, &sc, channels, queue.clone()),
|
||||
f => Err(format!("sample format {:?}", f)),
|
||||
}?;
|
||||
stream.play().map_err(|e| e.to_string())?;
|
||||
Ok(Out { _stream: stream, queue, rate })
|
||||
}
|
||||
|
||||
fn build<T: cpal::SizedSample + cpal::FromSample<f32>>(dev: &cpal::Device, sc: &cpal::StreamConfig, channels: usize, queue: Arc<Mutex<VecDeque<f32>>>) -> Result<cpal::Stream, String> {
|
||||
let channels = channels.max(1);
|
||||
dev.build_output_stream(
|
||||
sc.clone(),
|
||||
move |out: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
let mut q = queue.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for frame in out.chunks_mut(channels) {
|
||||
// an empty queue (a stalled frame) plays silence
|
||||
let (l, r) = if q.len() >= 2 { (q.pop_front().unwrap_or(0.0), q.pop_front().unwrap_or(0.0)) } else { (0.0, 0.0) };
|
||||
for (c, s) in frame.iter_mut().enumerate() {
|
||||
let v = match (channels, c) {
|
||||
(1, _) => (l + r) * 0.5,
|
||||
(_, 0) => l,
|
||||
(_, 1) => r,
|
||||
_ => 0.0,
|
||||
};
|
||||
*s = T::from_sample(v);
|
||||
}
|
||||
}
|
||||
},
|
||||
|e| eprintln!("DungeonCraft: sound output: {}", e),
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// called once per frame: tops the queue up to AHEAD seconds
|
||||
pub fn pump() {
|
||||
OUT.with(|o| {
|
||||
let o = o.borrow();
|
||||
let Some(o) = o.as_ref() else { return };
|
||||
let want = (o.rate * AHEAD) as usize;
|
||||
let have = o.queue.lock().unwrap_or_else(|e| e.into_inner()).len() / 2;
|
||||
if have >= want {
|
||||
return;
|
||||
}
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
let (mv, sv, muted) = (s.music_vol, s.sfx_vol, s.muted);
|
||||
if let Some(e) = s.engine.as_mut() {
|
||||
e.render(want - have, mv, sv, muted);
|
||||
o.queue.lock().unwrap_or_else(|e| e.into_inner()).extend(e.buf.iter().copied());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
// Automated balancing: whole games played by the AI brain on every side, headless and
|
||||
// deterministic, many seeds each. Run with
|
||||
// cargo test --release balance_ -- --ignored --nocapture --test-threads=4
|
||||
// and read the tables (docs/BALANCE.md keeps the findings).
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::game::*;
|
||||
use crate::rival::{Rival, PARTNER_GIFT_EVERY, RIVAL_CORE_HP};
|
||||
|
||||
const STEP: f32 = 0.05;
|
||||
|
||||
// the master on side 0 gets the same brain as the rivals
|
||||
fn pilot(g: &mut Game, difficulty: u8, focus: Option<u8>) {
|
||||
let core = g.map.core;
|
||||
// like every AI master: the 9x9 starting area and a portal of its own beside it
|
||||
g.carve_master_area(0, core);
|
||||
g.add_master_portal(0, core);
|
||||
g.rivals.push(Rival { side: 0, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0, focus, defending: false });
|
||||
}
|
||||
|
||||
// the other corners of a square map for up to three more masters (centre of the core)
|
||||
fn corners(w: i32, h: i32) -> [(i32, i32); 3] {
|
||||
[(w - 29, 13), (w - 29, h - 15), (15, h - 15)]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Outcome {
|
||||
out_at: Vec<(u8, f32)>, // side, minute it fell
|
||||
score: Vec<(u8, f32)>, // side, strength at the end (alive ones)
|
||||
winner: Option<u8>,
|
||||
minutes: f32,
|
||||
}
|
||||
|
||||
fn alive(g: &Game, side: u8) -> bool {
|
||||
g.core_hp_of(side) > 0.0 && g.rivals.iter().any(|r| r.side == side && r.alive)
|
||||
}
|
||||
|
||||
fn score(g: &Game, side: u8) -> f32 {
|
||||
g.core_hp_of(side) * 2.0 + g.side_army(side) + g.gold_of(side) * 0.2
|
||||
}
|
||||
|
||||
// plays until one side is left or the time is up; side 0 falling does not stop it
|
||||
fn play(g: &mut Game, sides: &[u8], max_min: f32) -> Outcome {
|
||||
let mut o = Outcome::default();
|
||||
let steps = (max_min * 60.0 / STEP) as usize;
|
||||
for k in 0..steps {
|
||||
g.update(STEP);
|
||||
if g.state != 0 {
|
||||
g.state = 0; // the harness decides the end
|
||||
}
|
||||
if k % 20 == 0 {
|
||||
for &s in sides {
|
||||
if !o.out_at.iter().any(|x| x.0 == s) && !alive(g, s) {
|
||||
o.out_at.push((s, g.time / 60.0));
|
||||
}
|
||||
}
|
||||
let left: Vec<u8> = sides.iter().copied().filter(|&s| !o.out_at.iter().any(|x| x.0 == s)).collect();
|
||||
if left.len() <= 1 {
|
||||
o.winner = left.first().copied();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
o.minutes = g.time / 60.0;
|
||||
o.score = sides.iter().copied().filter(|&s| !o.out_at.iter().any(|x| x.0 == s)).map(|s| (s, score(g, s))).collect();
|
||||
if o.winner.is_none() {
|
||||
o.winner = o.score.iter().max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|x| x.0);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
fn arena(seed: u32, size: usize) -> Game {
|
||||
let mut g = Game::new(seed, size, size);
|
||||
g.gate_on = false; // no hero waves: masters against each other
|
||||
g.ai_difficulty = 1;
|
||||
g
|
||||
}
|
||||
|
||||
fn seeds() -> Vec<u32> {
|
||||
let n: u32 = std::env::var("BALANCE_SEEDS").ok().and_then(|s| s.parse().ok()).unwrap_or(6);
|
||||
(1..=n).map(|k| 1000 + k * 37).collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- difficulty duels
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_difficulty() {
|
||||
println!("\n== difficulty duels (2 masters, 112x112, no waves, 45 min) ==");
|
||||
println!("{:<14} {:>5} {:>5} {:>5} {:>8}", "pair", "A", "B", "draw", "min");
|
||||
// BALANCE_PAIR=a,b runs one pairing only
|
||||
let pairs: Vec<(u8, u8)> = match std::env::var("BALANCE_PAIR").ok().and_then(|p| p.split_once(',').map(|(a, b)| (a.parse().ok(), b.parse().ok()))) {
|
||||
Some((Some(a), Some(b))) => vec![(a, b)],
|
||||
_ => vec![(0, 1), (1, 2), (0, 2), (1, 1)],
|
||||
};
|
||||
for (a, b) in pairs {
|
||||
let (mut wa, mut wb, mut dr, mut mins) = (0, 0, 0, 0.0);
|
||||
for (k, seed) in seeds().into_iter().enumerate() {
|
||||
let mut g = arena(seed, 112);
|
||||
// swap who plays which side every other seed (side 0 has the map's own start)
|
||||
let (d0, d2) = if k % 2 == 0 { (a, b) } else { (b, a) };
|
||||
pilot(&mut g, d0, None);
|
||||
let (x, y) = corners(112, 112)[k % 3];
|
||||
g.add_rival(2, x, y, d2);
|
||||
let o = play(&mut g, &[0, 2], 45.0);
|
||||
mins += o.minutes;
|
||||
let a_side = if k % 2 == 0 { 0 } else { 2 };
|
||||
let decisive = o.out_at.len() == 1;
|
||||
match o.winner {
|
||||
Some(w) if decisive && w == a_side => wa += 1,
|
||||
Some(_) if decisive => wb += 1,
|
||||
_ => dr += 1,
|
||||
}
|
||||
}
|
||||
let n = seeds().len() as f32;
|
||||
println!("{:<14} {:>5} {:>5} {:>5} {:>8.1}", format!("{} vs {}", a, b), wa, wb, dr, mins / n);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- talent branches
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_talents() {
|
||||
use crate::talents::*;
|
||||
println!("\n== talent tournament (4 masters, medium, 128x128, no waves, 50 min) ==");
|
||||
let names = ["builder", "warlord", "warlock", "merchant"];
|
||||
let mut wins = [0usize; 4];
|
||||
let mut place_sum = [0.0f32; 4];
|
||||
let mut lv_sum = [0.0f32; 4];
|
||||
let sides = [0u8, 2, 3, 4];
|
||||
for (k, seed) in seeds().into_iter().enumerate() {
|
||||
let mut g = arena(seed, 128);
|
||||
// the branches move round the four starts from seed to seed
|
||||
let branch_of = |s: usize| ((s + k) % 4) as u8;
|
||||
pilot(&mut g, 1, Some(branch_of(0)));
|
||||
for (n, &(x, y)) in corners(128, 128).iter().enumerate() {
|
||||
g.add_rival(sides[n + 1], x, y, 1);
|
||||
g.rivals.last_mut().unwrap().focus = Some(branch_of(n + 1));
|
||||
}
|
||||
let o = play(&mut g, &sides, 50.0);
|
||||
// placement: the winner 1st, the survivors by score, the fallen by how long they lasted
|
||||
let mut order: Vec<u8> = Vec::new();
|
||||
let mut alive_sorted = o.score.clone();
|
||||
alive_sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||
order.extend(alive_sorted.iter().map(|x| x.0));
|
||||
let mut fallen = o.out_at.clone();
|
||||
fallen.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||
order.extend(fallen.iter().map(|x| x.0));
|
||||
for (place, &s) in order.iter().enumerate() {
|
||||
let n = sides.iter().position(|&x| x == s).unwrap();
|
||||
let b = branch_of(n) as usize;
|
||||
place_sum[b] += place as f32 + 1.0;
|
||||
if place == 0 {
|
||||
wins[b] += 1;
|
||||
}
|
||||
lv_sum[b] += g.talents_of(s).lv[b] as f32;
|
||||
}
|
||||
let _ = BRANCHES;
|
||||
}
|
||||
let n = seeds().len() as f32;
|
||||
println!("{:<10} {:>5} {:>10} {:>10}", "branch", "wins", "avg place", "avg level");
|
||||
for b in 0..4 {
|
||||
println!("{:<10} {:>5} {:>10.2} {:>10.1}", names[b], wins[b], place_sum[b] / n, lv_sum[b] / n);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- one master against the waves
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_waves() {
|
||||
println!("\n== one AI master against the hero waves (96x96, 40 min) ==");
|
||||
println!("{:<6} {:>8} {:>6} {:>7} {:>7} {:>7} {:>6}", "diff", "fell@min", "wave", "heroes", "lost", "gold", "army");
|
||||
for d in 0..3u8 {
|
||||
for seed in seeds() {
|
||||
let mut g = Game::new(seed, 96, 96);
|
||||
g.ai_difficulty = d;
|
||||
pilot(&mut g, d, None);
|
||||
let mut fell = None;
|
||||
for k in 0..((40.0 * 60.0 / STEP) as usize) {
|
||||
g.update(STEP);
|
||||
if g.state != 0 {
|
||||
if g.core_hp <= 0.0 {
|
||||
fell = Some(g.time / 60.0);
|
||||
break;
|
||||
}
|
||||
g.state = 0;
|
||||
}
|
||||
if std::env::var("BALANCE_TRACE").is_ok() && k % (120.0 / STEP) as usize == 0 {
|
||||
let rooms: usize = g.counts.iter().sum();
|
||||
let portal = (0..g.map.portals.len()).any(|p| g.map.portal_owner(p) == crate::map::Owner::Player);
|
||||
println!(" seed {} d{} min {:>4.1}: creatures {:>3} imps {:>2} gold {:>6} rooms {:>3} lair {:>3} farm {:>3} wave {:>2} core {:>5} portal {} heroes near {}",
|
||||
seed, d, g.time / 60.0, g.creature_count(), g.worker_count(), g.gold as i32, rooms, g.counts[1], g.counts[2], g.wave, g.core_hp as i32, portal,
|
||||
g.ents.iter().filter(|e| e.alive && e.active_hero()).count());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:<6} {:>8} {:>6} {:>7} {:>7} {:>7} {:>6}",
|
||||
d,
|
||||
fell.map_or("-".to_string(), |m| format!("{:.1}", m)),
|
||||
g.wave,
|
||||
g.stats.get(crate::stats::HEROES) as i32,
|
||||
g.stats.get(crate::stats::LOST) as i32,
|
||||
g.gold as i32,
|
||||
g.side_army(0) as i32
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// where the heroes of a wave really break in, against the marked entry points
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn balance_entry_points() {
|
||||
use crate::map::*;
|
||||
println!("\n== planned entry vs. where the heroes step onto the master's ground ==");
|
||||
for seed in seeds() {
|
||||
let mut g = Game::new(seed, 96, 96);
|
||||
pilot(&mut g, 1, None);
|
||||
let mut planned: Vec<(i32, i32)> = Vec::new();
|
||||
let mut first_in: Option<(i32, i32)> = None;
|
||||
let mut wave = 0;
|
||||
for _ in 0..((20.0 * 60.0 / STEP) as usize) {
|
||||
g.update(STEP);
|
||||
if g.state != 0 {
|
||||
break;
|
||||
}
|
||||
if !g.planned_entries.is_empty() {
|
||||
planned = g.planned_entries.clone();
|
||||
}
|
||||
if g.wave != wave {
|
||||
wave = g.wave;
|
||||
first_in = None;
|
||||
}
|
||||
if first_in.is_none() {
|
||||
if let Some(e) = g.ents.iter().find(|e| e.alive && e.active_hero() && g.map.get(e.x as i32, e.y as i32).map_or(false, |t| t.owner == Owner::Player)) {
|
||||
first_in = Some((e.x as i32, e.y as i32));
|
||||
let d = planned.iter().map(|&(px, py)| (((px - e.x as i32).pow(2) + (py - e.y as i32).pow(2)) as f32).sqrt()).fold(f32::MAX, f32::min);
|
||||
let wp = g.ents.iter().filter(|e| e.alive && e.active_hero()).filter(|e| e.waypoint.is_some()).count();
|
||||
let heroes = g.ents.iter().filter(|e| e.alive && e.active_hero()).count();
|
||||
let job = format!("{:?}", e.job);
|
||||
println!("seed {} wave {}: planned {:?} first in at {:?} -> {:.1} tiles off; heroes with waypoint {}/{}; its job {}", seed, wave, planned, first_in.unwrap(), d, wp, heroes, job);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-5
@@ -16,7 +16,7 @@ pub const KINDS: [&str; 30] = [
|
||||
];
|
||||
|
||||
// ground a Terrain block can make
|
||||
pub const TERRAIN: [&str; 6] = ["floor", "earth", "rock", "water", "lava", "gold"];
|
||||
pub const TERRAIN: [&str; 7] = ["floor", "earth", "rock", "water", "lava", "gold", "grove"];
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "b")]
|
||||
@@ -42,6 +42,8 @@ pub enum Block {
|
||||
Terrain { zone: String, kind: String },
|
||||
Kill { zone: String, side: i32 },
|
||||
Trigger { name: String, on: bool },
|
||||
Weather { kind: String, secs: Val }, // the weather above ground (secs 0: until changed again)
|
||||
TalentXp { amount: Val }, // experience for the player's talents
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -70,6 +72,7 @@ pub enum Val {
|
||||
Or { a: Box<Val>, b: Box<Val> },
|
||||
Not { a: Box<Val> },
|
||||
EventKind { kind: String }, // the unit that set off the trigger is of this kind
|
||||
WeatherIs { kind: String }, // the weather above ground is ...
|
||||
}
|
||||
|
||||
pub const MATH_OPS: [&str; 4] = ["+", "-", "*", "/"];
|
||||
@@ -89,7 +92,7 @@ impl Val {
|
||||
|
||||
pub fn ty(&self) -> Ty {
|
||||
match self {
|
||||
Val::Bool { .. } | Val::Cmp { .. } | Val::And { .. } | Val::Or { .. } | Val::Not { .. } | Val::EventKind { .. } => Ty::Bool,
|
||||
Val::Bool { .. } | Val::Cmp { .. } | Val::And { .. } | Val::Or { .. } | Val::Not { .. } | Val::EventKind { .. } | Val::WeatherIs { .. } => Ty::Bool,
|
||||
_ => Ty::Num,
|
||||
}
|
||||
}
|
||||
@@ -113,6 +116,7 @@ impl Val {
|
||||
"or" => Val::Or { a: Box::new(Val::Bool { b: true }), b: Box::new(Val::Bool { b: false }) },
|
||||
"not" => Val::Not { a: Box::new(Val::Bool { b: true }) },
|
||||
"eventkind" => Val::EventKind { kind: "Knight".into() },
|
||||
"weatheris" => Val::WeatherIs { kind: "rain".into() },
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -135,16 +139,20 @@ impl Val {
|
||||
Val::Or { .. } => "or",
|
||||
Val::Not { .. } => "not",
|
||||
Val::EventKind { .. } => "eventkind",
|
||||
Val::WeatherIs { .. } => "weatheris",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value kinds by type, for the value picker
|
||||
pub const NUM_VALUES: [&str; 9] = ["num", "var", "gold", "mana", "time", "creatures", "inzone", "random", "math"];
|
||||
pub const BOOL_VALUES: [&str; 6] = ["cmp", "and", "or", "not", "bool", "eventkind"];
|
||||
pub const BOOL_VALUES: [&str; 7] = ["cmp", "and", "or", "not", "bool", "eventkind", "weatheris"];
|
||||
|
||||
// the weather a block can set or ask for (weather.rs: FAIR, RAIN, FOG, STORM)
|
||||
pub const WEATHER: [&str; 4] = ["fair", "rain", "fog", "storm"];
|
||||
|
||||
// statement blocks in the palette: (category, kind)
|
||||
pub const PALETTE: [(&str, &str); 21] = [
|
||||
pub const PALETTE: [(&str, &str); 23] = [
|
||||
("say", "message"),
|
||||
("say", "objective"),
|
||||
("say", "hint"),
|
||||
@@ -156,6 +164,8 @@ pub const PALETTE: [(&str, &str); 21] = [
|
||||
("world", "camera"),
|
||||
("world", "gold"),
|
||||
("world", "mana"),
|
||||
("world", "weather"),
|
||||
("world", "talentxp"),
|
||||
("logic", "if"),
|
||||
("logic", "repeat"),
|
||||
("logic", "until"),
|
||||
@@ -185,6 +195,8 @@ impl Block {
|
||||
"camera" => Block::Camera { zone: String::new() },
|
||||
"gold" => Block::Gold { amount: n(500.0) },
|
||||
"mana" => Block::Mana { amount: n(100.0) },
|
||||
"weather" => Block::Weather { kind: "storm".into(), secs: n(0.0) },
|
||||
"talentxp" => Block::TalentXp { amount: n(300.0) },
|
||||
"if" => Block::If { cond: Val::template("cmp").unwrap(), then: Vec::new(), els: Vec::new() },
|
||||
"repeat" => Block::Repeat { times: n(3.0), body: Vec::new() },
|
||||
"until" => Block::Until { cond: Val::template("cmp").unwrap(), body: Vec::new() },
|
||||
@@ -212,6 +224,8 @@ impl Block {
|
||||
Block::Camera { .. } => "camera",
|
||||
Block::Gold { .. } => "gold",
|
||||
Block::Mana { .. } => "mana",
|
||||
Block::Weather { .. } => "weather",
|
||||
Block::TalentXp { .. } => "talentxp",
|
||||
Block::If { .. } => "if",
|
||||
Block::Repeat { .. } => "repeat",
|
||||
Block::Until { .. } => "until",
|
||||
@@ -258,6 +272,7 @@ fn check_val(v: &Val, want: Ty, n: &Names, path: &[usize], out: &mut Vec<Problem
|
||||
Val::Creatures { kind, .. } if !kind.is_empty() && !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::InZone { zone, .. } if !n.zones.iter().any(|z| z == zone) => out.push(p("blk.err_zone")),
|
||||
Val::EventKind { kind } if !KINDS.contains(&kind.as_str()) => out.push(p("blk.err_kind")),
|
||||
Val::WeatherIs { kind } if !WEATHER.contains(&kind.as_str()) => out.push(p("blk.err_weather")),
|
||||
Val::Random { a, b } | Val::Math { a, b, .. } => {
|
||||
check_val(a, Ty::Num, n, path, out);
|
||||
check_val(b, Ty::Num, n, path, out);
|
||||
@@ -295,7 +310,13 @@ fn check_list(list: &[Block], n: &Names, path: &mut Vec<usize>, out: &mut Vec<Pr
|
||||
}
|
||||
}
|
||||
Block::Wave { count } => check_val(count, Ty::Num, n, path, out),
|
||||
Block::Gold { amount } | Block::Mana { amount } => check_val(amount, Ty::Num, n, path, out),
|
||||
Block::Gold { amount } | Block::Mana { amount } | Block::TalentXp { amount } => check_val(amount, Ty::Num, n, path, out),
|
||||
Block::Weather { kind, secs } => {
|
||||
if !WEATHER.contains(&kind.as_str()) {
|
||||
out.push(p("blk.err_weather", path));
|
||||
}
|
||||
check_val(secs, Ty::Num, n, path, out);
|
||||
}
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
if !valid_var(name) {
|
||||
out.push(p("blk.err_var", path));
|
||||
|
||||
+21
-1
@@ -543,7 +543,13 @@ impl BlockEdit {
|
||||
let r = lay.take(CW() * 10.0);
|
||||
im.text_field(gfx, fid(path, 6), r, name, 16);
|
||||
}
|
||||
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx),
|
||||
Block::Wave { count } | Block::Gold { amount: count } | Block::Mana { amount: count } | Block::TalentXp { amount: count } => self.value(im, gfx, count, Ty::Num, path, 1, lay, ctx),
|
||||
Block::Weather { kind, secs } => {
|
||||
weather_dd(im, gfx, kind, fid(path, 2), lay);
|
||||
lay.word(gfx, &tr("blk.w_for"));
|
||||
self.value(im, gfx, secs, Ty::Num, path, 1, lay, ctx);
|
||||
lay.word(gfx, &tr("blk.w_seconds"));
|
||||
}
|
||||
Block::SetVar { name, value } | Block::AddVar { name, value } => {
|
||||
let r = lay.take(CW() * 12.0);
|
||||
im.text_field(gfx, fid(path, 0), r, name, 24);
|
||||
@@ -713,8 +719,22 @@ impl BlockEdit {
|
||||
lay.word(gfx, &tr("blk.v.eventkind"));
|
||||
self.kind_dd(im, gfx, kind, false, path, sub(0), lay);
|
||||
}
|
||||
Val::WeatherIs { kind } => {
|
||||
lay.word(gfx, &tr("blk.v.weatheris"));
|
||||
weather_dd(im, gfx, kind, fid(path, sub(0)), lay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a dropdown with the kinds of weather
|
||||
fn weather_dd(im: &mut Im, gfx: &Gfx, kind: &mut String, id: u64, lay: &mut Lay) {
|
||||
let opts: Vec<String> = WEATHER.iter().map(|w| tr(&format!("weather.{}", w))).collect();
|
||||
let cur = tr(&format!("weather.{}", kind));
|
||||
let r = lay.take(CW() * 12.0);
|
||||
if let Some(c) = im.dropdown(gfx, id, r, &cur, &opts) {
|
||||
*kind = WEATHER[c].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// where the next field of a block goes: left to right, into a new row at the block's edge
|
||||
|
||||
@@ -143,6 +143,40 @@ pub fn coop_campaign() -> CampaignDoc {
|
||||
],
|
||||
});
|
||||
}
|
||||
// the rogues of the Order in the later missions: thieves from mission 7, an inquisitor
|
||||
// from mission 15; the last mission (above ground) brews a storm
|
||||
let lv = n((1 + i / 3) as f64);
|
||||
if i >= 6 {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "thieves".into(),
|
||||
kind: TriggerKind::Timer { secs: 420.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![Block::Message { text: tr("coop.raid") }, Block::Spawn { side: 1, kind: "Thief".into(), count: n(2.0), level: lv.clone(), place: Place::Zone(ZONES[i % 3].0.into()), name: String::new() }],
|
||||
});
|
||||
}
|
||||
if i >= 14 {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "inquisitor".into(),
|
||||
kind: TriggerKind::Timer { secs: 600.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![
|
||||
Block::Message { text: tr("coop.raid") },
|
||||
Block::Spawn { side: 1, kind: "Inquisitor".into(), count: n(1.0), level: lv.clone(), place: Place::Zone(ZONES[(i + 1) % 3].0.into()), name: String::new() },
|
||||
Block::Spawn { side: 1, kind: "Monk".into(), count: n(2.0), level: lv.clone(), place: Place::Zone(ZONES[(i + 1) % 3].0.into()), name: String::new() },
|
||||
],
|
||||
});
|
||||
}
|
||||
if i + 1 == specs.len() {
|
||||
sc.triggers.push(TriggerDoc {
|
||||
name: "storm".into(),
|
||||
kind: TriggerKind::Timer { secs: 480.0, every: false },
|
||||
once: true,
|
||||
enabled: true,
|
||||
script: vec![Block::Weather { kind: "storm".into(), secs: n(120.0) }],
|
||||
});
|
||||
}
|
||||
if let Some((kind, level, name, zone)) = sp.boss {
|
||||
sc.start.push(Block::Spawn { side: 1, kind: kind.into(), count: n(1.0), level: n(level as f64), place: Place::Zone(ZONES[zone].0.into()), name: name.into() });
|
||||
// the boss falls: the mission is won once the goals (if any) are done too
|
||||
|
||||
+62
-4
@@ -62,17 +62,26 @@ pub enum Sfx {
|
||||
Voice4,
|
||||
Voice5,
|
||||
Voice6,
|
||||
// weather, rogues and groves
|
||||
Thunder,
|
||||
Steal,
|
||||
Snare,
|
||||
Sermon,
|
||||
Rain,
|
||||
Wind,
|
||||
Spores,
|
||||
}
|
||||
|
||||
pub const VOICE_SYLLABLES: [Sfx; 6] = [Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6];
|
||||
|
||||
pub const SFX_ALL: [Sfx; 42] = [
|
||||
pub const SFX_ALL: [Sfx; 49] = [
|
||||
Sfx::Dig, Sfx::GoldDig, Sfx::DigDone, Sfx::Claim, Sfx::Deposit, Sfx::Build, Sfx::Sell, Sfx::MeleeHit,
|
||||
Sfx::Shoot, Sfx::Death, Sfx::HeroDeath, Sfx::Lightning, Sfx::Heal, Sfx::Summon, Sfx::PortalArrive,
|
||||
Sfx::WaveAlarm, Sfx::WaveStart, Sfx::CoreHit, Sfx::GateHit, Sfx::GateDefenders, Sfx::LevelUp,
|
||||
Sfx::Pickup, Sfx::Drop, Sfx::Payday, Sfx::FlagSet, Sfx::Victory, Sfx::Defeat, Sfx::Click, Sfx::Error,
|
||||
Sfx::Reinforce, Sfx::Cannon, Sfx::Magic, Sfx::Capture, Sfx::Convert, Sfx::Slap, Sfx::Punch,
|
||||
Sfx::Voice1, Sfx::Voice2, Sfx::Voice3, Sfx::Voice4, Sfx::Voice5, Sfx::Voice6,
|
||||
Sfx::Thunder, Sfx::Steal, Sfx::Snare, Sfx::Sermon, Sfx::Rain, Sfx::Wind, Sfx::Spores,
|
||||
];
|
||||
|
||||
impl Sfx {
|
||||
@@ -152,6 +161,8 @@ pub struct St {
|
||||
|
||||
pub const RUNE_TOOL: usize = 20; // index of the rune forge tower in ui::TOOLS
|
||||
pub const SUMMON_TOOL: usize = 11; // index of summoning in ui::TOOLS
|
||||
pub const LIGHTNING_TOOL: usize = 9; // lightning in ui::TOOLS
|
||||
pub const HEAL_TOOL: usize = 10; // healing in ui::TOOLS
|
||||
|
||||
pub fn st(k: EK) -> St {
|
||||
let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty };
|
||||
@@ -294,6 +305,10 @@ pub struct Ent {
|
||||
#[serde(default)]
|
||||
pub name: u32, // seed of the creature's name (names.rs), 0: none (heroes, siege engines)
|
||||
#[serde(default)]
|
||||
pub kills: u32, // heroes this creature has slain (names.rs: the hall of the fallen)
|
||||
#[serde(default)]
|
||||
pub bane: u8, // what hurt it last: sprite index + 1 of a unit kind, or names::BANE_*
|
||||
#[serde(default)]
|
||||
pub ab_cd: f32, // cooldown of the learned ability
|
||||
#[serde(default)]
|
||||
pub mood_t: f32, // seconds until the next mood check (likes and hatreds)
|
||||
@@ -384,6 +399,8 @@ impl Ent {
|
||||
invis: 0.0,
|
||||
plague: 0.0,
|
||||
name: 0,
|
||||
kills: 0,
|
||||
bane: 0,
|
||||
ab_cd: 0.0,
|
||||
mood_t: 0.0,
|
||||
guard: false,
|
||||
@@ -643,6 +660,8 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub memorials: Vec<crate::names::Memorial>, // stones for fallen creatures (names.rs)
|
||||
#[serde(default)]
|
||||
pub hall: Vec<crate::names::Fallen>, // the local master's fallen of this game (names.rs)
|
||||
#[serde(default)]
|
||||
pub trading: crate::traders::Trading, // the visiting trader (traders.rs)
|
||||
#[serde(default)]
|
||||
pub happenings: Option<crate::events::Events>, // random events (free games and versus), see events.rs
|
||||
@@ -1000,6 +1019,7 @@ impl Game {
|
||||
bridgework: Default::default(),
|
||||
weather: Default::default(),
|
||||
memorials: Vec::new(),
|
||||
hall: Vec::new(),
|
||||
trading: Default::default(),
|
||||
happenings: None,
|
||||
snares: Vec::new(),
|
||||
@@ -2782,6 +2802,11 @@ impl Game {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hit_for_test(&mut self, e: &mut Ent, j: usize) {
|
||||
self.hit(e, j);
|
||||
}
|
||||
|
||||
fn hit(&mut self, e: &mut Ent, j: usize) {
|
||||
// the Warlord talent of the attacker's master
|
||||
let boost = if !e.hero && Owner::MASTER_SIDES.contains(&e.side) { self.talents_of(e.side).damage() } else { 1.0 };
|
||||
@@ -2901,6 +2926,10 @@ impl Game {
|
||||
let t = &mut self.ents[j];
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour;
|
||||
t.flash = 0.15;
|
||||
t.bane = crate::gfx::sprite_index(e.kind) as u8 + 1;
|
||||
if t.hp <= 0.0 && t.hero && t.captive == 0 {
|
||||
e.kills += 1;
|
||||
}
|
||||
if t.creature() && !matches!(t.job, Job::Attack(_)) {
|
||||
t.job = Job::Attack(my_id);
|
||||
t.path.clear();
|
||||
@@ -3016,6 +3045,13 @@ impl Game {
|
||||
}
|
||||
self.release_all(id);
|
||||
self.hand.retain(|&h| h != id);
|
||||
// a named creature lost in lava or water is remembered too
|
||||
if !hero {
|
||||
let mut dead = self.ents[j].clone();
|
||||
let lava = self.map.get(x as i32, y as i32).map_or(false, |t| t.river() == Some(Kind::Lava));
|
||||
dead.bane = if lava { crate::names::BANE_LAVA } else { crate::names::BANE_WATER };
|
||||
self.raise_memorial(&dead);
|
||||
}
|
||||
self.fx(3, x, y, 0.0, 0.0, 1.0, 0xff2060ff);
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.5, 0xff30a0ff);
|
||||
self.sfx(Sfx::Death, x, y);
|
||||
@@ -4867,6 +4903,13 @@ impl Game {
|
||||
if (e.x - wx as f32 - 0.5).abs() < 2.5 && (e.y - wy as f32 - 0.5).abs() < 2.5 {
|
||||
e.waypoint = None; // broke into the realm: now for the core
|
||||
e.path.clear();
|
||||
} else if let Some(&next) = e.path.first() {
|
||||
// the realm grew into the way (imps claim fast): round it again
|
||||
let realm = self.map.t[idx(wx, wy)].owner;
|
||||
if self.map.t[next].owner == realm && next != idx(wx, wy) && self.map.t[e.tile()].owner != realm {
|
||||
e.path.clear();
|
||||
e.repath = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
@@ -4874,7 +4917,8 @@ impl Game {
|
||||
let path = match e.waypoint {
|
||||
Some((wx, wy)) => {
|
||||
let goal = idx(wx, wy);
|
||||
let p = self.map.search(cur, self.map.hero_cost(), |i| i == goal, true);
|
||||
// round the realm from outside: in through the marked entry, not before
|
||||
let p = self.entry_path(cur, goal).or_else(|| self.map.search(cur, self.map.hero_cost(), |i| i == goal, true));
|
||||
if p.is_none() {
|
||||
e.waypoint = None;
|
||||
}
|
||||
@@ -4939,14 +4983,28 @@ impl Game {
|
||||
scored.push(((x, y), score));
|
||||
}
|
||||
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let first = scored[0].0;
|
||||
// only places the heroes can reach from the gate without crossing the realm first
|
||||
let gate = idx(gx, gy);
|
||||
let mut tried = 0;
|
||||
let mut ok = |g: &Game, p: (i32, i32)| {
|
||||
tried += 1;
|
||||
tried > 12 || g.entry_path(gate, idx(p.0, p.1)).is_some()
|
||||
};
|
||||
let Some(first) = scored.iter().map(|s| s.0).find(|&p| ok(self, p)) else { return };
|
||||
self.planned_entries.push(first);
|
||||
// second entry point at least 10 tiles away (used by waves of 6+ heroes)
|
||||
if let Some(&(second, _)) = scored.iter().find(|((x, y), _)| (x - first.0).pow(2) + (y - first.1).pow(2) >= 100) {
|
||||
if let Some(second) = scored.iter().map(|s| s.0).filter(|&(x, y)| (x - first.0).pow(2) + (y - first.1).pow(2) >= 100).find(|&p| ok(self, p)) {
|
||||
self.planned_entries.push(second);
|
||||
}
|
||||
}
|
||||
|
||||
// a hero's way to an entry tile that stays off the realm (the master's ground) until the entry
|
||||
pub(crate) fn entry_path(&self, from: usize, goal: usize) -> Option<Vec<usize>> {
|
||||
let owner = self.map.t[goal].owner;
|
||||
let cost = self.map.hero_cost();
|
||||
self.map.search(from, |i| if i != goal && i != from && self.map.t[i].owner == owner { None } else { cost(i) }, |i| i == goal, true)
|
||||
}
|
||||
|
||||
// compass direction of a tile as seen from the core (language key)
|
||||
pub fn direction_key(&self, (x, y): (i32, i32)) -> &'static str {
|
||||
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
|
||||
|
||||
+31
-6
@@ -1,4 +1,6 @@
|
||||
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
|
||||
// Windows release builds are a window program: no console opens beside the game
|
||||
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
||||
mod achievements;
|
||||
mod ai;
|
||||
@@ -30,6 +32,7 @@ mod props;
|
||||
mod research;
|
||||
mod rival;
|
||||
mod rogues;
|
||||
mod balance;
|
||||
mod talents;
|
||||
mod weather;
|
||||
mod names;
|
||||
@@ -141,7 +144,16 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
|
||||
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel | Tool::Drawbridge | Tool::Wall, Some(t)) => inp.ldown = Some(t),
|
||||
(Tool::Hand, _) => {
|
||||
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
|
||||
let Some(j) = j else { return };
|
||||
let Some(j) = j else {
|
||||
// a memorial stone: the hall of the fallen opens
|
||||
let (gx, gy) = (wx.floor() as i32, wy.floor() as i32);
|
||||
if map::inb(gx, gy) && app.game.memorial_at(map::idx(gx, gy)).is_some() {
|
||||
ui.show_overview = true;
|
||||
ui.ov_fallen = true;
|
||||
ui.ov_scroll = 0;
|
||||
}
|
||||
return;
|
||||
};
|
||||
let (id, hero, dwarf) = (app.game.ents[j].id, app.game.ents[j].hero, app.game.ents[j].dwarf());
|
||||
if dwarf {
|
||||
return;
|
||||
@@ -919,8 +931,12 @@ async fn main() {
|
||||
let mut voice = voice::Voice::new();
|
||||
let mut was_briefing = false;
|
||||
let mut cache_t = 0.0f32;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
audio::native::start();
|
||||
|
||||
loop {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
audio::native::pump();
|
||||
let raw_dt = get_frame_time();
|
||||
let dt = raw_dt.min(0.1);
|
||||
let now = get_time();
|
||||
@@ -976,8 +992,12 @@ async fn main() {
|
||||
// 1080 device rows in landscape; a smaller screen draws in device pixels)
|
||||
let need: u32 = 900;
|
||||
// measured on the shorter side: upright (portrait) the width gets those pixels and the
|
||||
// height grows with the screen, so the world and the menus keep their size
|
||||
let short = rows * need.div_ceil(rows);
|
||||
// height grows with the screen, so the world and the menus keep their size. The setting
|
||||
// "interface size" picks the interface pixels per world pixel: large 2, medium 3, small 4
|
||||
let short = match app.settings.ui_factor {
|
||||
k @ 2..=4 => rows * k as u32,
|
||||
_ => rows * need.div_ceil(rows),
|
||||
};
|
||||
let (w, h) = (screen_width().max(1.0), screen_height().max(1.0));
|
||||
let vrows = if h > w { (short as f32 * h / w).round() as u32 } else { short };
|
||||
ui::set_virtual_rows(vrows);
|
||||
@@ -1483,7 +1503,7 @@ async fn main() {
|
||||
MenuAction::Save | MenuAction::FileSave => {
|
||||
let name = save_file_name();
|
||||
platform::download(&name, app.autosave_json().as_bytes());
|
||||
app.set_status(i18n::trf("save.saved", &[name]), now);
|
||||
app.set_status(i18n::trf("save.saved", &[platform::saved_as(&name)]), now);
|
||||
}
|
||||
MenuAction::CloudSave(id, name) => {
|
||||
if cloud.save(id.as_deref(), &name, &app.autosave_json()) {
|
||||
@@ -1530,7 +1550,7 @@ async fn main() {
|
||||
MenuAction::CampaignSave => {
|
||||
let name = menu.campaign_doc.file_name();
|
||||
platform::download(&name, menu.campaign_doc.save().as_bytes());
|
||||
app.set_status(i18n::trf("campaign.saved", &[name]), now);
|
||||
app.set_status(i18n::trf("campaign.saved", &[platform::saved_as(&name)]), now);
|
||||
}
|
||||
MenuAction::CampaignEditName => {
|
||||
if let Some(value) = prompt_line(&i18n::tr("campaign.prompt_name"), &menu.campaign_doc.name, 60) {
|
||||
@@ -1988,6 +2008,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
for ev in app.game.events.drain(..) {
|
||||
sounds.play(ev, vec2(app.cam.rx, app.cam.rz), right, hear, now);
|
||||
}
|
||||
sounds.ambience(&app.game, (app.cam.rx, app.cam.rz), now);
|
||||
// a won mission goes into the campaign progress (with the carry-over); a co-op mission of an
|
||||
// editor campaign keeps no progress (its index is not a built-in mission)
|
||||
if app.coop_editor {
|
||||
@@ -2334,7 +2355,11 @@ app.pending_briefing = briefing_of(&ed.doc, i);
|
||||
ui.show_help = false;
|
||||
r3d.invalidate();
|
||||
}
|
||||
editor::EdAction::SaveFile => platform::download(&ed.doc.file_name(), ed.doc.save().as_bytes()),
|
||||
editor::EdAction::SaveFile => {
|
||||
let name = ed.doc.file_name();
|
||||
platform::download(&name, ed.doc.save().as_bytes());
|
||||
ed.set_status(i18n::trf("campaign.saved", &[platform::saved_as(&name)]));
|
||||
}
|
||||
editor::EdAction::OpenFile => platform::open_file(),
|
||||
editor::EdAction::ServerSave => {
|
||||
if !cloud.available() {
|
||||
|
||||
+15
-8
@@ -141,13 +141,12 @@ fn item_value(it: Item, s: &Settings) -> String {
|
||||
_ => "val.medium",
|
||||
}),
|
||||
Item::Lights => s.max_lights.to_string(),
|
||||
Item::UiScale => {
|
||||
if s.ui_factor == 0 {
|
||||
trf("val.auto", &[crate::ui::F().to_string()])
|
||||
} else {
|
||||
format!("{}x", s.ui_factor)
|
||||
}
|
||||
}
|
||||
Item::UiScale => tr(match s.ui_factor {
|
||||
2 => "val.large",
|
||||
3 => "val.medium",
|
||||
4 => "val.small",
|
||||
_ => "val.auto_plain",
|
||||
}),
|
||||
Item::DebugFps => onoff(s.debug_fps),
|
||||
Item::DebugFrameTime => onoff(s.debug_frame_time),
|
||||
Item::DebugPing => onoff(s.debug_ping),
|
||||
@@ -202,7 +201,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
Item::Outlines => s.outlines = !s.outlines,
|
||||
Item::WallDetail => s.wall_detail = cycle(&[1u8, 2, 3], s.wall_detail, d),
|
||||
Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d),
|
||||
Item::UiScale => s.ui_factor = cycle(&[0u8, 1, 2, 3, 4], s.ui_factor, d),
|
||||
Item::UiScale => s.ui_factor = cycle(&[0u8, 2, 3, 4], if [0, 2, 3, 4].contains(&s.ui_factor) { s.ui_factor } else { 0 }, d),
|
||||
Item::RenderScale => s.pixel_rows = cycle(&[270u16, 360, 480], if [270, 360, 480].contains(&s.pixel_rows) { s.pixel_rows } else { 360 }, d),
|
||||
Item::DebugFps => s.debug_fps = !s.debug_fps,
|
||||
Item::DebugFrameTime => s.debug_frame_time = !s.debug_frame_time,
|
||||
@@ -304,6 +303,7 @@ enum Btn {
|
||||
MatchChat,
|
||||
Bestiary,
|
||||
Achievements,
|
||||
AchHall, // achievements page: switch to the hall of the fallen and back
|
||||
Tutorial,
|
||||
BestTab(usize),
|
||||
BestSel(i32),
|
||||
@@ -455,6 +455,7 @@ pub struct Menu {
|
||||
scroll: usize,
|
||||
rows_visible: usize,
|
||||
pub profile: crate::achievements::Profile, // shown on the achievements page
|
||||
ach_hall: bool, // the achievements page shows the hall of the fallen
|
||||
buttons: Vec<(Rect, Btn)>,
|
||||
lang_open: Option<Rect>, // the language dropdown is open below/above this field
|
||||
lang_scroll: usize, // first language shown in the open dropdown
|
||||
@@ -498,6 +499,7 @@ impl Menu {
|
||||
focus: 0,
|
||||
error: None,
|
||||
match_ui: MatchUi::default(),
|
||||
ach_hall: false,
|
||||
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
|
||||
match_size: 128,
|
||||
coop_editor: false,
|
||||
@@ -940,6 +942,11 @@ impl Menu {
|
||||
}
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::AchHall) => {
|
||||
self.ach_hall = !self.ach_hall;
|
||||
self.scroll = 0;
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Achievements) => {
|
||||
self.page = Some(Page::Achievements { from_title });
|
||||
self.scroll = 0;
|
||||
|
||||
@@ -15,7 +15,13 @@ impl Menu {
|
||||
let rows = 40.min((sh / CH()) as i32 - 2);
|
||||
let x = grid_center(sw, cols as f32 * CW()).max(0.0);
|
||||
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.achievements"), FRAME_HI);
|
||||
let title = if self.ach_hall { tr("hall.title") } else { tr("menu.achievements") };
|
||||
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
|
||||
if self.ach_hall {
|
||||
self.draw_hall_page(gfx, x, y, cols, rows);
|
||||
self.ach_buttons(gfx, x, y, rows);
|
||||
return;
|
||||
}
|
||||
let list = all();
|
||||
let done = list.iter().filter(|a| self.profile.unlocked.contains_key(&a.0)).count();
|
||||
let head = trf("ach.count", &[done.to_string(), list.len().to_string(), crate::achievements::fmt_count(self.profile.total("minutes"))]);
|
||||
@@ -57,14 +63,75 @@ impl Menu {
|
||||
}
|
||||
}
|
||||
// scrolling and back
|
||||
let _ = ix;
|
||||
self.ach_buttons(gfx, x, y, rows);
|
||||
}
|
||||
|
||||
// scrolling, the switch between achievements and the hall, back
|
||||
fn ach_buttons(&mut self, gfx: &Gfx, x: f32, y: f32, rows: i32) {
|
||||
let ix = x + CW() * 3.0;
|
||||
let by = y + (rows - 3) as f32 * CH();
|
||||
let (mx, my) = mouse();
|
||||
let bw = 14.0 * CW();
|
||||
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::Back, tr("menu.back"))];
|
||||
let switch = if self.ach_hall { tr("menu.achievements") } else { tr("hall.title") };
|
||||
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::AchHall, switch), (Btn::Back, tr("menu.back"))];
|
||||
let bw = bw.min((sw() - ix - CW() * 4.0) / 4.0 - CW());
|
||||
for (k, (b, label)) in items.iter().enumerate() {
|
||||
let r = Rect::new(ix + k as f32 * (bw + CW()), by, bw, CH() * 2.0);
|
||||
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, *b));
|
||||
}
|
||||
}
|
||||
|
||||
// the hall of the fallen over all games: newest first, two lines each
|
||||
fn draw_hall_page(&mut self, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
|
||||
let hall = &self.profile.hall;
|
||||
gfx.text(&trf("hall.count", &[hall.len().to_string()]), x + CW() * 3.0, y + CH() * 1.6, S(), GOLD);
|
||||
let top = y + CH() * 3.5;
|
||||
let bottom = y + (rows - 4) as f32 * CH();
|
||||
let row_h = CH() * 2.4;
|
||||
self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1);
|
||||
self.scroll = self.scroll.min(hall.len().saturating_sub(self.rows_visible));
|
||||
let ix = x + CW() * 3.0;
|
||||
let maxc = (cols - 6) as usize;
|
||||
if hall.is_empty() {
|
||||
gfx.text(&tr("hall.empty"), ix, top, S(), DIM);
|
||||
}
|
||||
for (k, f) in hall.iter().rev().enumerate().skip(self.scroll).take(self.rows_visible) {
|
||||
let ry = top + (k - self.scroll) as f32 * row_h;
|
||||
let name = format!("{} ({})", crate::names::full_name(f.kind, f.name, f.level), trf("info.level", &[tr(crate::game::st(f.kind).name), f.level.to_string()]));
|
||||
gfx.text(&name.chars().take(maxc).collect::<String>(), ix, ry, S(), FRAME_HI);
|
||||
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
|
||||
let line = format!("{}{} · {}", crate::names::cause(f.bane), kills, date(f.when));
|
||||
gfx.text(&line.chars().take(maxc * 2).collect::<String>(), ix + CW() * 2.0, ry + CH(), SMALL(), DIM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a unix time as a date (year-month-day)
|
||||
fn date(unix: u64) -> String {
|
||||
if unix == 0 {
|
||||
return String::new();
|
||||
}
|
||||
// days since 1970-01-01 to a civil date (Howard Hinnant's algorithm)
|
||||
let z = (unix / 86400) as i64 + 719468;
|
||||
let era = z.div_euclid(146097);
|
||||
let doe = z - era * 146097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
|
||||
format!("{:04}-{:02}-{:02}", y, m, d)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn dates_are_civil() {
|
||||
assert_eq!(super::date(0), "");
|
||||
assert_eq!(super::date(1_798_675_200), "2026-12-31");
|
||||
assert_eq!(super::date(1_709_164_800), "2024-02-29");
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,8 @@ enum Act {
|
||||
Terrain(String, String),
|
||||
Kill(String, i32),
|
||||
Trigger(String, bool),
|
||||
Weather(u8, f64),
|
||||
TalentXp(f64),
|
||||
}
|
||||
|
||||
// the triggering unit (for "where the unit is" and "the unit is a ...")
|
||||
@@ -539,6 +541,12 @@ impl Game {
|
||||
Val::Or { a, b } => (self.cond(s, a, ev) || self.cond(s, b, ev)) as i32 as f64,
|
||||
Val::Not { a } => (!self.cond(s, a, ev)) as i32 as f64,
|
||||
Val::EventKind { kind } => (ev.kind.is_some() && ev.kind == kind_by_name(kind)) as i32 as f64,
|
||||
Val::WeatherIs { kind } => {
|
||||
// the weather felt now (a change counts once it has come in)
|
||||
let w = &self.weather;
|
||||
let now = if w.level > 0.5 { w.shown } else { crate::weather::FAIR };
|
||||
(crate::blocks::WEATHER.iter().position(|k| k == kind) == Some(now as usize)) as i32 as f64
|
||||
}
|
||||
};
|
||||
if r.is_finite() {
|
||||
r
|
||||
@@ -568,6 +576,8 @@ impl Game {
|
||||
Block::Wave { count } => Act::Wave(self.eval(s, count, ev).round() as i64),
|
||||
Block::Gold { amount } => Act::Gold(self.eval(s, amount, ev)),
|
||||
Block::Mana { amount } => Act::Mana(self.eval(s, amount, ev)),
|
||||
Block::Weather { kind, secs } => Act::Weather(crate::blocks::WEATHER.iter().position(|k| k == kind).unwrap_or(0) as u8, self.eval(s, secs, ev)),
|
||||
Block::TalentXp { amount } => Act::TalentXp(self.eval(s, amount, ev)),
|
||||
Block::SetVar { name, value } => {
|
||||
let v = self.eval(s, value, ev);
|
||||
s.vars.insert(name.clone(), v);
|
||||
@@ -824,6 +834,13 @@ impl Game {
|
||||
}
|
||||
}
|
||||
Act::Mana(n) => self.mana = (self.mana + n as f32).clamp(0.0, self.mana_max()),
|
||||
// the weather comes (softly) and stays the given seconds (0: until a block changes it)
|
||||
Act::Weather(k, secs) => {
|
||||
self.weather.kind = k;
|
||||
self.weather.left = if secs > 0.0 { secs as f32 } else { 1.0e9 };
|
||||
self.weather.strike_t = self.weather.strike_t.min(5.0);
|
||||
}
|
||||
Act::TalentXp(n) => self.add_master_xp(n.max(0.0) as f32),
|
||||
Act::Win => {
|
||||
self.state = 1;
|
||||
self.script_msg("msg.mission_won", vec![]);
|
||||
@@ -854,6 +871,7 @@ impl Game {
|
||||
"water" => Kind::Water,
|
||||
"lava" => Kind::Lava,
|
||||
"gold" => Kind::Gold,
|
||||
"grove" => Kind::Grove,
|
||||
_ => Kind::Floor,
|
||||
};
|
||||
for i in zone_tiles(&z) {
|
||||
@@ -960,6 +978,28 @@ mod tests {
|
||||
assert!(g.script.as_ref().unwrap().error.is_none(), "{:?}", g.script.as_ref().unwrap().error);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weather_and_talent_blocks_work() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.add_surface(3, 0.5, 2);
|
||||
let scripts = MissionScripts {
|
||||
start: vec![
|
||||
Block::Weather { kind: "storm".into(), secs: Val::num(0.0) },
|
||||
Block::TalentXp { amount: Val::num(400.0) },
|
||||
Block::Wait { secs: Val::num(25.0) },
|
||||
Block::If { cond: Val::WeatherIs { kind: "storm".into() }, then: vec![Block::SetVar { name: "storm".into(), value: Val::num(1.0) }], els: vec![] },
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
g.script = Some(ScriptState::new(scripts, vec![], 7));
|
||||
run(&mut g, 30.0);
|
||||
assert_eq!(g.weather.kind, crate::weather::STORM);
|
||||
assert!(g.weather.left > 1.0e8, "it stays");
|
||||
assert!(g.talents.xp >= 400.0 && g.talents.free() >= 1);
|
||||
assert_eq!(g.script.as_ref().unwrap().vars.get("storm"), Some(&1.0), "the condition sees the storm");
|
||||
assert!(crate::blocks::check(&g.script.as_ref().unwrap().scripts.start, &crate::blocks::Names { zones: &[], triggers: &[], vars: &[] }).iter().all(|p| p.key != "blk.err_weather"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zone_trigger_spawns_and_the_victory_script_wins() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
|
||||
+101
-7
@@ -8,9 +8,45 @@ use crate::i18n::{tr, trf};
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// the fallen get a stone from this level on; at most MAX_MEMORIALS stones (the oldest go)
|
||||
pub const MEMORIAL_LEVEL: u32 = 3;
|
||||
pub const MAX_MEMORIALS: usize = 24;
|
||||
// the fallen are remembered in the hall from HALL_LEVEL on; only the greatest (MEMORIAL_LEVEL)
|
||||
// get a stone on the map, at most MAX_MEMORIALS of them (the oldest go)
|
||||
pub const HALL_LEVEL: u32 = 3;
|
||||
pub const MEMORIAL_LEVEL: u32 = 10;
|
||||
pub const MAX_MEMORIALS: usize = 30;
|
||||
|
||||
// what killed a creature besides another unit (Ent::bane)
|
||||
pub const BANE_SNARE: u8 = 250;
|
||||
pub const BANE_LIGHTNING: u8 = 251;
|
||||
pub const BANE_LAVA: u8 = 252;
|
||||
pub const BANE_WATER: u8 = 253;
|
||||
|
||||
// an entry of the hall of the fallen (in the game and in the player's profile)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Fallen {
|
||||
pub kind: EK,
|
||||
pub name: u32,
|
||||
pub level: u32,
|
||||
pub kills: u32,
|
||||
pub bane: u8,
|
||||
pub time: f32, // game seconds when it fell
|
||||
#[serde(default)]
|
||||
pub when: u64, // unix time (set when it goes into the profile)
|
||||
}
|
||||
|
||||
// how it fell, in words
|
||||
pub fn cause(bane: u8) -> String {
|
||||
match bane {
|
||||
BANE_SNARE => tr("cause.snare"),
|
||||
BANE_LIGHTNING => tr("cause.lightning"),
|
||||
BANE_LAVA => tr("cause.lava"),
|
||||
BANE_WATER => tr("cause.water"),
|
||||
0 => tr("cause.fell"),
|
||||
k => match crate::gfx::SPRITE_KINDS.get(k as usize - 1) {
|
||||
Some(&kind) => trf("cause.slain", &[tr(st(kind).name)]),
|
||||
None => tr("cause.fell"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Memorial {
|
||||
@@ -100,9 +136,17 @@ impl Game {
|
||||
// a named creature of some standing fell: a stone with its name on its master's ground, as
|
||||
// near to where it fell as there is plain floor
|
||||
pub(crate) fn raise_memorial(&mut self, e: &Ent) -> bool {
|
||||
if e.hero || e.name == 0 || e.level < MEMORIAL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
|
||||
if e.hero || e.name == 0 || e.level < HALL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
|
||||
return false;
|
||||
}
|
||||
// the hall of the fallen keeps the local master's (also without room for a stone)
|
||||
if e.side == self.local_side {
|
||||
self.hall.push(Fallen { kind: e.kind, name: e.name, level: e.level, kills: e.kills, bane: e.bane, time: self.time, when: 0 });
|
||||
}
|
||||
// a stone only for the greatest
|
||||
if e.level < MEMORIAL_LEVEL {
|
||||
return false; // the usual message tells of it
|
||||
}
|
||||
let owner = Owner::of_side(e.side);
|
||||
let (x, y) = (e.x as i32, e.y as i32);
|
||||
let taken: Vec<usize> = self.memorials.iter().map(|m| m.tile).collect();
|
||||
@@ -168,6 +212,50 @@ mod tests {
|
||||
assert_eq!(g.ents.iter().find(|e| e.id == h).unwrap().name, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_hall_remembers_kills_and_how_they_fell() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy + 4..cy + 8 {
|
||||
for x in cx..cx + 6 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
let mut t = crate::achievements::Tracker::new(Default::default());
|
||||
let ctx = crate::achievements::Context { coop: false, editor_test: false };
|
||||
g.time = 1.0;
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_000);
|
||||
let ogre = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 5);
|
||||
let knight = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 5.5, 1);
|
||||
// the ogre slays the knight
|
||||
let (oj, kj) = (g.ents.iter().position(|e| e.id == ogre).unwrap(), g.ents.iter().position(|e| e.id == knight).unwrap());
|
||||
g.ents[kj].hp = 1.0;
|
||||
let mut o = g.ents[oj].clone();
|
||||
g.hit_for_test(&mut o, kj);
|
||||
assert_eq!(o.kills, 1, "a hero slain");
|
||||
g.ents[oj] = o;
|
||||
// a second knight brings the ogre down
|
||||
let k2 = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 6.5, 1);
|
||||
let k2j = g.ents.iter().position(|e| e.id == k2).unwrap();
|
||||
let oj = g.ents.iter().position(|e| e.id == ogre).unwrap();
|
||||
g.ents[oj].hp = 1.0;
|
||||
let mut k = g.ents[k2j].clone();
|
||||
g.hit_for_test(&mut k, oj);
|
||||
assert_eq!(g.hall.len(), 1);
|
||||
let f = &g.hall[0];
|
||||
assert_eq!((f.kind, f.kills), (EK::Ogre, 1));
|
||||
assert_eq!(f.bane as u32, crate::gfx::sprite_index(EK::Knight) + 1);
|
||||
assert!(cause(f.bane).contains(&crate::i18n::tr("unit.knight")));
|
||||
// into the profile, once
|
||||
g.time = 100.0;
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_100);
|
||||
t.update(&g, &ctx, 6.0, 1_800_000_200);
|
||||
assert_eq!(t.profile.hall.len(), 1);
|
||||
assert_eq!(t.profile.hall[0].when, 1_800_000_100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fallen_get_a_memorial_stone() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
@@ -179,12 +267,18 @@ mod tests {
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
let id = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 4);
|
||||
let id = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 10);
|
||||
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||
g.kill(j);
|
||||
assert_eq!(g.memorials.len(), 1);
|
||||
let m = &g.memorials[0];
|
||||
assert_eq!((m.kind, m.level), (EK::Ogre, 4));
|
||||
let m = g.memorials[0].clone();
|
||||
assert_eq!((m.kind, m.level), (EK::Ogre, 10));
|
||||
// level 4: in the hall, but no stone on the map
|
||||
let o4 = g.spawn(EK::Ogre, false, cx as f32 + 3.5, cy as f32 + 5.5, 4);
|
||||
let j = g.ents.iter().position(|e| e.id == o4).unwrap();
|
||||
g.kill(j);
|
||||
assert_eq!(g.memorials.len(), 1);
|
||||
assert_eq!(g.hall.len(), 2);
|
||||
assert!(g.memorial_at(m.tile).is_some());
|
||||
// a level 1 kobold is not remembered
|
||||
let k = g.spawn(EK::Kobold, false, cx as f32 + 2.5, cy as f32 + 5.5, 1);
|
||||
|
||||
+1
-1
@@ -365,7 +365,7 @@ impl Game {
|
||||
// the seat keeps its master (dungeon, gold, creatures): the brain (ai.rs) plays it
|
||||
let core = self.core_of_side(side);
|
||||
let difficulty = self.ai_difficulty;
|
||||
self.rivals.push(crate::rival::Rival { side, core, hp: crate::rival::RIVAL_CORE_HP, alive: true, radius: 6, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: crate::rival::PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0 });
|
||||
self.rivals.push(crate::rival::Rival { side, core, hp: crate::rival::RIVAL_CORE_HP, alive: true, radius: 6, grow_t: 0.0, spawn_t: 0.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: crate::rival::PARTNER_GIFT_EVERY, think_t: 1.0, attacking: false, army0: 0.0, focus: None, defending: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+128
-14
@@ -1,7 +1,7 @@
|
||||
// Browser integration via our own miniquad JS plugin (www/dc_plugin.js):
|
||||
// persistent key/value storage (localStorage), a cache that is written to storage when the
|
||||
// page is hidden or closed, file download, file upload and HTTP requests.
|
||||
// Native builds use simple fallbacks.
|
||||
// Native builds keep the storage as files in the data directory (see imp below).
|
||||
|
||||
// Result of an HTTP request: status 0 means the service could not be reached
|
||||
pub struct HttpResult {
|
||||
@@ -113,6 +113,11 @@ mod imp {
|
||||
unsafe { dc_cache_set(key.as_ptr(), key.len() as u32, val.as_ptr(), val.len() as u32) }
|
||||
}
|
||||
|
||||
// where download() put a file, for the status line: the browser's download folder
|
||||
pub fn saved_as(name: &str) -> String {
|
||||
name.to_string()
|
||||
}
|
||||
|
||||
pub fn download(name: &str, data: &[u8]) {
|
||||
unsafe { dc_download(name.as_ptr(), name.len() as u32, data.as_ptr(), data.len() as u32) }
|
||||
}
|
||||
@@ -188,14 +193,84 @@ mod imp {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod imp {
|
||||
use std::cell::RefCell;
|
||||
// Desktop builds: the storage keys are files in the data directory, savegames and
|
||||
// campaigns are written to its "saves" folder, the system file dialog opens files.
|
||||
use std::collections::HashMap;
|
||||
thread_local! {
|
||||
static STORE: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static STORE: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
|
||||
static PICKED: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||
static PICKING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
// DUNGEONCRAFT_DATA, else the usual per-user application data folder
|
||||
pub fn data_dir() -> PathBuf {
|
||||
let env = |k: &str| std::env::var_os(k).filter(|v| !v.is_empty()).map(PathBuf::from);
|
||||
if let Some(d) = env("DUNGEONCRAFT_DATA") {
|
||||
return d;
|
||||
}
|
||||
if cfg!(target_os = "windows") {
|
||||
if let Some(d) = env("APPDATA") {
|
||||
return d.join("DungeonCraft");
|
||||
}
|
||||
} else if cfg!(target_os = "macos") {
|
||||
if let Some(h) = env("HOME") {
|
||||
return h.join("Library/Application Support/DungeonCraft");
|
||||
}
|
||||
} else if let Some(d) = env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share"))) {
|
||||
return d.join("dungeoncraft");
|
||||
}
|
||||
PathBuf::from("dungeoncraft-data")
|
||||
}
|
||||
|
||||
pub fn saves_dir() -> PathBuf {
|
||||
data_dir().join("saves")
|
||||
}
|
||||
|
||||
// a storage key as a file name: only harmless characters
|
||||
pub(super) fn key_file(key: &str) -> String {
|
||||
let name: String = key.chars().map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') { c } else { '_' }).collect();
|
||||
if name.is_empty() || name.starts_with('.') {
|
||||
format!("_{}", name)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
// writes through a temporary file, so a crash never leaves half a file
|
||||
fn write_file(path: &std::path::Path, data: &[u8]) -> std::io::Result<()> {
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
let tmp = path.with_extension("tmp");
|
||||
std::fs::write(&tmp, data)?;
|
||||
std::fs::rename(&tmp, path)
|
||||
}
|
||||
|
||||
pub fn storage_get(key: &str) -> Option<String> {
|
||||
STORE.with(|s| s.borrow().get(key).cloned())
|
||||
let mut store = STORE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let store = store.get_or_insert_with(HashMap::new);
|
||||
if let Some(v) = store.get(key) {
|
||||
return Some(v.clone());
|
||||
}
|
||||
let v = std::fs::read_to_string(data_dir().join("store").join(key_file(key))).ok()?;
|
||||
store.insert(key.to_string(), v.clone());
|
||||
Some(v)
|
||||
}
|
||||
|
||||
pub fn storage_set(key: &str, val: &str) {
|
||||
STORE.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).insert(key.to_string(), val.to_string());
|
||||
let path = data_dir().join("store").join(key_file(key));
|
||||
if let Err(e) = write_file(&path, val.as_bytes()) {
|
||||
eprintln!("DungeonCraft: cannot write {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// there is no "page left" moment on the desktop: write at once
|
||||
pub fn cache_on_leave(key: &str, val: &str) {
|
||||
storage_set(key, val);
|
||||
}
|
||||
|
||||
pub fn prompt(_title: &str, _default: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
@@ -208,19 +283,47 @@ mod imp {
|
||||
pub fn fetch_download_state() -> i32 {
|
||||
0
|
||||
}
|
||||
pub fn storage_set(key: &str, val: &str) {
|
||||
STORE.with(|s| s.borrow_mut().insert(key.to_string(), val.to_string()));
|
||||
}
|
||||
pub fn cache_on_leave(key: &str, val: &str) {
|
||||
storage_set(key, val);
|
||||
}
|
||||
|
||||
// a "download" lands in the saves folder
|
||||
pub fn download(name: &str, data: &[u8]) {
|
||||
let _ = std::fs::write(name, data);
|
||||
let path = saves_dir().join(key_file(name));
|
||||
if let Err(e) = write_file(&path, data) {
|
||||
eprintln!("DungeonCraft: cannot write {}: {}", path.display(), e);
|
||||
}
|
||||
pub fn open_file() {}
|
||||
}
|
||||
|
||||
// where download() put a file, for the status line
|
||||
pub fn saved_as(name: &str) -> String {
|
||||
saves_dir().join(key_file(name)).display().to_string()
|
||||
}
|
||||
|
||||
fn pick() {
|
||||
let dir = saves_dir();
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let file = rfd::FileDialog::new().set_directory(&dir).pick_file();
|
||||
if let Some(bytes) = file.and_then(|f| std::fs::read(f).ok()) {
|
||||
*PICKED.lock().unwrap_or_else(|e| e.into_inner()) = Some(bytes);
|
||||
}
|
||||
PICKING.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// opens the system file dialog (one at a time); poll with take_file()
|
||||
pub fn open_file() {
|
||||
if PICKING.swap(true, std::sync::atomic::Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
// macOS dialogs must run on the main thread; elsewhere the game keeps running
|
||||
if cfg!(target_os = "macos") {
|
||||
pick();
|
||||
} else if std::thread::Builder::new().name("file dialog".into()).spawn(pick).is_err() {
|
||||
PICKING.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_file() -> Option<Vec<u8>> {
|
||||
None
|
||||
PICKED.lock().unwrap_or_else(|e| e.into_inner()).take()
|
||||
}
|
||||
|
||||
// no savegame service in native builds: every request fails like an unreachable server
|
||||
pub fn http(_method: &str, _url: &str, _body: &[u8]) -> u32 {
|
||||
0
|
||||
@@ -270,3 +373,14 @@ pub use imp::*;
|
||||
pub extern "C" fn dungeoncraft_crate_version() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn storage_keys_become_harmless_file_names() {
|
||||
assert_eq!(super::imp::key_file("dc.settings"), "dc.settings");
|
||||
assert_eq!(super::imp::key_file("../etc/passwd"), "_.._etc_passwd");
|
||||
assert_eq!(super::imp::key_file("a b/c"), "a_b_c");
|
||||
assert_eq!(super::imp::key_file(""), "_");
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -3377,12 +3377,21 @@ fn draw_entry_warnings(g: &Game, t: f32, n: usize) {
|
||||
let line = [255, 90, 60, (120.0 + 120.0 * p) as u8];
|
||||
let mut mb = MB::new();
|
||||
for &(ex, ey) in &g.planned_entries {
|
||||
let realm = g.map.get(ex, ey).map_or(Owner::Player, |t| t.owner);
|
||||
// the entry tile itself: a pulsing red plate (also where an open corridor leads in)
|
||||
let c = vec3(ex as f32 + 0.5, 0.03, ey as f32 + 0.5);
|
||||
mb.quad(
|
||||
[c + vec3(-0.5, 0.0, -0.5), c + vec3(0.5, 0.0, -0.5), c + vec3(0.5, 0.0, 0.5), c + vec3(-0.5, 0.0, 0.5)],
|
||||
[Vec2::ZERO; 4],
|
||||
[col(vec3(1.0, 0.16, 0.12) * (0.6 + 0.6 * p), 0.45 + 0.35 * p); 4],
|
||||
[Vec3::Y; 4],
|
||||
);
|
||||
for dy in -2..=2 {
|
||||
for dx in -2..=2 {
|
||||
let (x, y) = (ex + dx, ey + dy);
|
||||
let Some(tile) = g.map.get(x, y) else { continue };
|
||||
// walls of the realm's border near the entry point (not rock)
|
||||
let border = DIR4.iter().any(|&(ax, ay)| g.map.get(x + ax, y + ay).map_or(false, |o| o.owner == Owner::Player && !o.solid()));
|
||||
let border = DIR4.iter().any(|&(ax, ay)| g.map.get(x + ax, y + ay).map_or(false, |o| o.owner == realm && !o.solid()));
|
||||
if tile.diggable() && border && dx.abs() + dy.abs() <= 3 && mb.v.len() < 30000 {
|
||||
block_shell(&mut mb, &g.map, x, y, n, fill, line, 1.0, 0.04);
|
||||
}
|
||||
|
||||
+5
-1
@@ -33,6 +33,10 @@ pub struct Rival {
|
||||
pub attacking: bool, // the attack flag is set on an enemy heart
|
||||
#[serde(default)]
|
||||
pub army0: f32, // army strength when the attack began
|
||||
#[serde(default)]
|
||||
pub focus: Option<u8>, // balance runs: this master learns only one talent branch
|
||||
#[serde(default)]
|
||||
pub defending: bool, // the rally flag is on intruders in the dungeon (ai.rs)
|
||||
}
|
||||
|
||||
impl Rival {
|
||||
@@ -98,7 +102,7 @@ impl Game {
|
||||
let core = (cx - 1, cy - 1);
|
||||
self.carve_master_area(side, core);
|
||||
self.add_master_portal(side, core);
|
||||
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 20.0, spawn_t: 5.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 2.0, attacking: false, army0: 0.0 });
|
||||
self.rivals.push(Rival { side, core, hp: RIVAL_CORE_HP, alive: true, radius: 5, grow_t: 20.0, spawn_t: 5.0, attack_t: 0.0, difficulty, power: 0.0, gift_t: PARTNER_GIFT_EVERY, think_t: 2.0, attacking: false, army0: 0.0, focus: None, defending: false });
|
||||
self.adopt_master(side, core, difficulty, true);
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -94,6 +94,7 @@ impl Game {
|
||||
}
|
||||
e.carry += got;
|
||||
self.fx(5, e.x, e.y, 0.0, 0.0, 0.5, 0xff30d0ff);
|
||||
self.sfx(Sfx::Steal, e.x, e.y);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -192,10 +193,11 @@ impl Game {
|
||||
let e = &mut self.ents[j];
|
||||
e.stun = e.stun.max(SNARE_STUN);
|
||||
e.hp -= SNARE_DAMAGE;
|
||||
e.bane = crate::names::BANE_SNARE;
|
||||
e.flash = 0.2;
|
||||
let (x, y, side, kind, dead) = (e.x, e.y, e.side, e.kind, e.hp <= 0.0);
|
||||
self.fx(2, x, y, 0.0, 0.0, 0.4, 0xff4040ff);
|
||||
self.sfx(Sfx::MeleeHit, x, y);
|
||||
self.sfx(Sfx::Snare, x, y);
|
||||
if side == self.local_side {
|
||||
self.tell_local("msg.snare_caught", vec![format!("@{}", st(kind).name)], None);
|
||||
}
|
||||
@@ -244,7 +246,7 @@ impl Game {
|
||||
self.shot(e.x, e.y, 0.6, x, y, 0.45, 0.35, 0xff40d0ff);
|
||||
self.style_last_fx(crate::particles::ST_HOLY);
|
||||
self.fx(4, x, y, 0.0, 0.0, 0.9, 0xffffeecc);
|
||||
self.sfx(Sfx::Capture, x, y);
|
||||
self.sfx(Sfx::Sermon, x, y);
|
||||
if side == self.local_side {
|
||||
self.tell_local("msg.converted", vec![format!("@{}", st(kind).name)], Some(Sfx::WaveAlarm));
|
||||
}
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ pub struct Settings {
|
||||
pub outlines: bool,
|
||||
pub wall_detail: u8, // 1 low, 2 medium, 3 high
|
||||
pub max_lights: u8, // 8, 16 or 24
|
||||
pub ui_factor: u8, // interface size: 0 = automatic, 1..4 = fixed integer factor
|
||||
pub ui_factor: u8, // interface size: 0 = automatic, 2 large, 3 medium, 4 small (interface pixels per world pixel)
|
||||
pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files)
|
||||
pub pixel_rows: u16, // pixel look: rows of the world on every screen (270, 360, 480)
|
||||
pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off
|
||||
|
||||
+43
@@ -164,6 +164,31 @@ fn def(s: Sfx, r: f32) -> Vec<f32> {
|
||||
Sfx::Voice4 => l(&[seg(Square(0.15), 900.0, 980.0, 0.05, 0.13, 10.0), vib(seg(Square(0.2), 700.0, 560.0, 0.06, 0.14, 8.0), 0.04)], 65),
|
||||
Sfx::Voice5 => mix(vec![l(&[vib(seg(Square(0.25), 470.0, 560.0, 0.1, 0.15, 5.0), 0.06)], 66), l(&[seg(Noise, 3000.0, 2000.0, 0.025, 0.06, 30.0)], 67)]),
|
||||
Sfx::Voice6 => l(&[vib(seg(Square(0.2), 680.0, 820.0, 0.06, 0.15, 9.0), 0.05), seg(Square(0.2), 820.0, 700.0, 0.04, 0.12, 12.0)], 68),
|
||||
// a long rumble rolling away after the crack
|
||||
Sfx::Thunder => mix(vec![
|
||||
l(&[seg(Noise, 420.0, 70.0, 2.4, 0.55, 1.1)], 70),
|
||||
l(&[vib(seg(Tri, 55.0, 32.0, 2.2, 0.5, 1.2), 0.08)], 71),
|
||||
l(&[seg(Noise, 160.0, 160.0, 0.35, 0.0, 0.0), seg(Noise, 160.0, 90.0, 1.4, 0.3, 1.6)], 72),
|
||||
]),
|
||||
// the thief's fingers in the coins: a quick, soft jingle
|
||||
Sfx::Steal => mix(vec![l(&arp(Square(0.25), &[2093.0, 1760.0, 2349.0], 0.04, 0.1), 73), l(&[seg(Noise, 7000.0, 7000.0, 0.03, 0.08, 60.0)], 74)]),
|
||||
// iron jaws snap shut
|
||||
Sfx::Snare => mix(vec![l(&[seg(Noise, 8000.0, 3000.0, 0.04, 0.5, 50.0)], 75), l(&[seg(Square(0.5), 1400.0, 300.0, 0.09, 0.22, 25.0)], 76)]),
|
||||
// the inquisitor's sermon: a short, hollow chant
|
||||
Sfx::Sermon => mix(vec![
|
||||
l(&[vib(seg(Sine, 330.0, 330.0, 0.7, 0.28, 2.0), 0.02)], 77),
|
||||
l(&[vib(seg(Sine, 392.0, 415.0, 0.7, 0.2, 2.0), 0.02)], 78),
|
||||
l(&[vib(seg(Sine, 494.0, 494.0, 0.7, 0.16, 2.0), 0.03)], 79),
|
||||
]),
|
||||
// rain on the land: fades in and out, played over and over while it rains
|
||||
Sfx::Rain => mix(vec![
|
||||
l(&[seg(Noise, 7000.0, 7000.0, 0.3, 0.04, -6.0), seg(Noise, 7000.0, 7000.0, 0.9, 0.24, 0.0), seg(Noise, 7000.0, 7000.0, 0.3, 0.24, 6.0)], 80),
|
||||
l(&[seg(Noise, 2500.0, 2500.0, 0.3, 0.02, -6.0), seg(Noise, 2500.0, 2500.0, 0.9, 0.12, 0.0), seg(Noise, 2500.0, 2500.0, 0.3, 0.12, 6.0)], 81),
|
||||
]),
|
||||
// wind over the land: a slow, howling sweep
|
||||
Sfx::Wind => l(&[seg(Noise, 300.0, 700.0, 0.6, 0.03, -3.0), seg(Noise, 700.0, 500.0, 1.4, 0.18, 0.2), seg(Noise, 500.0, 250.0, 0.6, 0.15, 3.0)], 82),
|
||||
// the spores of a grove: a faint chime
|
||||
Sfx::Spores => l(&arp(Sine, &[2093.0, 2637.0, 3136.0, 2637.0], 0.09, 0.07), 83),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,3 +196,21 @@ fn def(s: Sfx, r: f32) -> Vec<f32> {
|
||||
pub fn generate_all(rate: f32) -> Vec<Vec<f32>> {
|
||||
SFX_ALL.iter().map(|&s| def(s, rate)).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_effect_is_audible_and_does_not_clip() {
|
||||
let all = generate_all(22050.0);
|
||||
for (s, buf) in SFX_ALL.iter().zip(all.iter()) {
|
||||
let peak = buf.iter().fold(0.0f32, |m, v| m.max(v.abs()));
|
||||
assert!(!buf.is_empty() && peak > 0.02, "{:?} is silent", s);
|
||||
assert!(peak < 1.5, "{:?} is too loud: {}", s, peak);
|
||||
}
|
||||
// the rain loops: it is long enough to overlap when played every 1.25 s
|
||||
let rain = &all[Sfx::Rain.index()];
|
||||
assert!(rain.len() as f32 / 22050.0 > 1.3);
|
||||
}
|
||||
}
|
||||
+42
-1
@@ -8,11 +8,52 @@ use macroquad::prelude::*;
|
||||
|
||||
pub struct SoundBank {
|
||||
last: Vec<f64>,
|
||||
next_rain: f64,
|
||||
next_wind: f64,
|
||||
next_spores: f64,
|
||||
}
|
||||
|
||||
impl SoundBank {
|
||||
pub fn new() -> SoundBank {
|
||||
SoundBank { last: vec![-1.0; SFX_ALL.len()] }
|
||||
SoundBank { last: vec![-1.0; SFX_ALL.len()], next_rain: 0.0, next_wind: 0.0, next_spores: 0.0 }
|
||||
}
|
||||
|
||||
// background sounds where the camera looks: rain and wind over the land above ground (the
|
||||
// samples fade in and out and overlap a little), the chime of a mushroom grove now and then
|
||||
pub fn ambience(&mut self, g: &crate::game::Game, (cx, cz): (f32, f32), now: f64) {
|
||||
use crate::map::{idx, inb, Kind};
|
||||
let near = |r: i32, f: &dyn Fn(usize) -> bool| {
|
||||
let (x, z) = (cx as i32, cz as i32);
|
||||
let mut n = 0;
|
||||
for dz in (-r..=r).step_by(2) {
|
||||
for dx in (-r..=r).step_by(2) {
|
||||
if inb(x + dx, z + dz) && f(idx(x + dx, z + dz)) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
n
|
||||
};
|
||||
if g.state != 0 {
|
||||
return;
|
||||
}
|
||||
let outside = !g.map.sky.is_empty() && near(6, &|i| g.map.sky(i)) >= 6;
|
||||
let w = &g.weather;
|
||||
if outside && w.rain() > 0.05 && now >= self.next_rain {
|
||||
self.next_rain = now + 1.25;
|
||||
audio::play_sfx(Sfx::Rain.index(), 0.55 * w.rain(), 0.0);
|
||||
}
|
||||
let windy = if w.shown == crate::weather::STORM { w.level } else { w.fog() * 0.5 };
|
||||
if outside && windy > 0.05 && now >= self.next_wind {
|
||||
self.next_wind = now + 2.2;
|
||||
audio::play_sfx(Sfx::Wind.index(), 0.45 * windy, (now.sin() * 0.4) as f32);
|
||||
}
|
||||
if now >= self.next_spores {
|
||||
self.next_spores = now + 3.0 + (now * 7.3).sin().abs() * 3.0;
|
||||
if near(5, &|i| matches!(g.map.t[i].kind, Kind::Grove)) >= 3 {
|
||||
audio::play_sfx(Sfx::Spores.index(), 0.35, ((now * 1.7).sin() * 0.5) as f32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// minimum gap between two identical sounds (avoids a wall of noise)
|
||||
|
||||
+13
-5
@@ -73,10 +73,10 @@ impl Talents {
|
||||
}
|
||||
// Builder: imps dig and claim faster, rooms cost less
|
||||
pub fn dig_speed(&self) -> f32 {
|
||||
1.0 + 0.1 * self.level(BUILDER)
|
||||
1.0 + 0.15 * self.level(BUILDER)
|
||||
}
|
||||
pub fn room_cost(&self) -> f32 {
|
||||
1.0 - 0.06 * self.level(BUILDER)
|
||||
1.0 - 0.1 * self.level(BUILDER)
|
||||
}
|
||||
// Warlord: creatures hit harder and take less damage
|
||||
pub fn damage(&self) -> f32 {
|
||||
@@ -136,8 +136,16 @@ impl Game {
|
||||
self.sfx_ui(Sfx::LevelUp);
|
||||
}
|
||||
|
||||
// an AI master spends his points by himself: the branches in turn, the lowest first
|
||||
pub(crate) fn ai_talents(&mut self) {
|
||||
// an AI master spends his points by himself: the branches in turn, the lowest first (a balance
|
||||
// run may hold him to one branch)
|
||||
pub(crate) fn ai_talents(&mut self, focus: Option<u8>) {
|
||||
if let Some(b) = focus {
|
||||
let b = b as usize;
|
||||
while self.talents.free() > 0 && self.talents.lv[b] < MAX_LEVEL {
|
||||
self.talents.lv[b] += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
while self.talents.free() > 0 {
|
||||
let order = [WARLORD, BUILDER, MERCHANT, WARLOCK];
|
||||
let Some(&b) = order.iter().filter(|&&b| self.talents.lv[b] < MAX_LEVEL).min_by_key(|&&b| self.talents.lv[b]) else { break };
|
||||
@@ -213,7 +221,7 @@ mod tests {
|
||||
fn ai_masters_spend_their_points() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
g.talents.xp = point_xp(6);
|
||||
g.ai_talents();
|
||||
g.ai_talents(None);
|
||||
assert_eq!(g.talents.free(), 0);
|
||||
assert!(g.talents.lv.iter().all(|&l| l >= 1), "{:?}", g.talents.lv);
|
||||
}
|
||||
|
||||
@@ -461,6 +461,7 @@ enum TBtn {
|
||||
Learn(u8), // spend a point on a branch
|
||||
OvRow(u32),
|
||||
OvScroll(i32),
|
||||
OvFallen, // switch the overview between the living and the fallen
|
||||
GoldTo, // the next ally to receive gold
|
||||
Gold(u32), // send that much gold to him
|
||||
}
|
||||
@@ -491,7 +492,8 @@ pub struct Ui {
|
||||
pub show_talents: bool, // the master's talents
|
||||
pub coop_end: Option<(bool, bool, bool)>, // end of an editor co-op mission: (host, a next mission, asked for it)
|
||||
gold_to: usize, // the ally chosen in the creature overview
|
||||
ov_scroll: usize,
|
||||
pub ov_scroll: usize,
|
||||
pub ov_fallen: bool, // the overview shows the hall of the fallen
|
||||
ov_rect: Option<Rect>, // where the creature overview is (mouse wheel, swipe)
|
||||
pub show_langsel: bool,
|
||||
pub log: Vec<(Msg, f64)>,
|
||||
@@ -557,7 +559,7 @@ fn branch_text(t: &crate::talents::Talents, b: usize) -> String {
|
||||
let desc = |lv: u8| {
|
||||
let l = lv as i32;
|
||||
let (a, c) = match b {
|
||||
BUILDER => (10 * l, 6 * l),
|
||||
BUILDER => (15 * l, 10 * l),
|
||||
WARLORD => (5 * l, 5 * l),
|
||||
WARLOCK => (15 * l, 6 * l),
|
||||
_ => (6 * l, 6 * l),
|
||||
@@ -603,6 +605,7 @@ impl Ui {
|
||||
coop_end: None,
|
||||
gold_to: 0,
|
||||
ov_scroll: 0,
|
||||
ov_fallen: false,
|
||||
ov_rect: None,
|
||||
show_langsel: false,
|
||||
log: Vec::new(),
|
||||
@@ -702,6 +705,11 @@ impl Ui {
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Learn(b) => UiAction::LearnTalent(b),
|
||||
TBtn::OvFallen => {
|
||||
self.ov_fallen = !self.ov_fallen;
|
||||
self.ov_scroll = 0;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::OvScroll(d) => {
|
||||
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
|
||||
UiAction::Consumed
|
||||
@@ -1343,6 +1351,36 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
// the hall of the fallen of this game: name, kind, level, heroes slain, how it fell
|
||||
fn draw_hall(&mut self, g: &Game, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
|
||||
gfx.text(&tr("hall.title"), x + CW() * 2.0, y + CH(), S(), GOLD);
|
||||
let mut lines: Vec<(String, Color)> = Vec::new();
|
||||
for f in g.hall.iter().rev() {
|
||||
lines.push((format!("{:>2} {}", f.level, crate::names::full_name(f.kind, f.name, f.level)), FRAME_HI));
|
||||
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
|
||||
lines.push((format!(" {} · {}{} · {}", tr(st(f.kind).name), crate::names::cause(f.bane), kills, mmss(f.time)), DIM));
|
||||
}
|
||||
if lines.is_empty() {
|
||||
lines.push((tr("hall.empty"), DIM));
|
||||
}
|
||||
let first = y + CH() * 2.6;
|
||||
let visible = (rows - 4).max(1) as usize;
|
||||
self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible));
|
||||
let maxc = (cols - 4) as usize;
|
||||
for (n, (text, col)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) {
|
||||
let t: String = text.chars().take(maxc).collect();
|
||||
gfx.text(&t, x + CW() * 2.0, first + (n - self.ov_scroll) as f32 * CH(), S(), *col);
|
||||
}
|
||||
if lines.len() > visible {
|
||||
let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH() * 2.4, CW() * 2.0, CH() * 1.5);
|
||||
let down = Rect::new(x + (cols - 4) as f32 * CW(), y + (rows - 2) as f32 * CH(), CW() * 2.0, CH() * 1.5);
|
||||
gfx.text("\u{25b2}", up.x + CW() * 0.5, up.y, S(), FRAME_HI);
|
||||
gfx.text("\u{25bc}", down.x + CW() * 0.5, down.y, S(), FRAME_HI);
|
||||
self.touch_btns.push((up, TBtn::OvScroll(-2)));
|
||||
self.touch_btns.push((down, TBtn::OvScroll(2)));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
||||
let (sw, _) = (sw(), sh());
|
||||
let (mx, my) = mouse();
|
||||
@@ -1357,6 +1395,21 @@ impl Ui {
|
||||
let mine: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let wage: f32 = mine.iter().map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
let mood = if mine.is_empty() { 0.0 } else { mine.iter().map(|e| e.happy.clamp(0.0, 1.0)).sum::<f32>() / mine.len() as f32 };
|
||||
// the switch to the hall of the fallen (top right)
|
||||
{
|
||||
let label = if self.ov_fallen { tr("ov.living") } else { trf("ov.fallen", &[g.hall.len().to_string()]) };
|
||||
let bw = gfx.measure(&label, SMALL()) + CW() * 2.0;
|
||||
let r = Rect::new(x + (cols - 3) as f32 * CW() - bw, y + CH() * 0.8, bw, CH() * 1.4);
|
||||
let hov = r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, FRAME_HI);
|
||||
gfx.text(&label, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), FRAME_HI);
|
||||
self.touch_btns.push((r, TBtn::OvFallen));
|
||||
}
|
||||
if self.ov_fallen {
|
||||
self.draw_hall(g, gfx, x, y, cols, rows);
|
||||
return;
|
||||
}
|
||||
let head = trf("ov.summary", &[mine.len().to_string(), (wage as i32).to_string(), ((mood * 100.0) as i32).to_string()]);
|
||||
gfx.text(&head, x + CW() * 2.0, y + CH(), S(), GOLD);
|
||||
// a match with allies: a row to send them gold (the name switches between the allies)
|
||||
|
||||
@@ -145,6 +145,7 @@ impl Game {
|
||||
let e = &mut self.ents[j];
|
||||
e.hp -= BOLT_DAMAGE;
|
||||
e.flash = 0.3;
|
||||
e.bane = crate::names::BANE_LIGHTNING;
|
||||
let (x, y, side, kind, hero, dead) = (e.x, e.y, e.side, e.kind, e.hero, e.hp <= 0.0);
|
||||
if !hero && side == self.local_side {
|
||||
self.tell_local("msg.lightning_unit", vec![format!("@{}", st(kind).name)], None);
|
||||
@@ -175,6 +176,10 @@ impl Game {
|
||||
self.weather.flash = self.time;
|
||||
self.fx(1, x, y, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||
self.sfx(Sfx::Lightning, x, y);
|
||||
// the thunder rolls over the whole land
|
||||
if self.events.len() < 64 {
|
||||
self.events.push(SfxEvent { sfx: Sfx::Thunder, pos: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the desktop versions of DungeonCraft into dist/.
|
||||
|
||||
python3 tools/build_desktop.py # everything this machine can build
|
||||
python3 tools/build_desktop.py linux windows # only these
|
||||
|
||||
- linux: dist/DungeonCraft-<version>-x86_64.AppImage
|
||||
built with cargo-zigbuild against glibc 2.28, so it runs on distributions from about 2019 on;
|
||||
needs on the build machine: zig, cargo-zigbuild, the ALSA headers (alsa-lib / libasound2-dev);
|
||||
appimagetool is downloaded once into ~/.cache/dungeoncraft/
|
||||
- windows: dist/DungeonCraft-<version>-windows-x64.exe (one file, Windows 10 and newer)
|
||||
cross-compiled with cargo-zigbuild for x86_64-pc-windows-gnu
|
||||
- macos: dist/DungeonCraft-<version>-macos.zip (DungeonCraft.app, Apple silicon + Intel)
|
||||
only on a Mac (Apple's SDK is needed); the app is signed ad hoc, not notarised
|
||||
|
||||
Everything the game needs (graphics, music, languages) is inside the program; nothing else is packed.
|
||||
The tools: `cargo install --locked cargo-zigbuild`, zig from the package manager or `mise install zig@0.15`
|
||||
(found on PATH, in $CARGO_ZIGBUILD_ZIG_PATH or in mise's install folder), and the Rust targets
|
||||
(`rustup target add ...`, the script tells which).
|
||||
"""
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
DIST = ROOT / "dist"
|
||||
CACHE = pathlib.Path(os.environ.get("XDG_CACHE_HOME") or pathlib.Path.home() / ".cache") / "dungeoncraft"
|
||||
ICON = ROOT / "www" / "icons" / "icon-512.png"
|
||||
GLIBC = "2.28"
|
||||
APPIMAGETOOL_URL = "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
|
||||
|
||||
DESKTOP_ENTRY = """[Desktop Entry]
|
||||
Type=Application
|
||||
Name=DungeonCraft
|
||||
Comment=Dig. Build. Defend. Conquer.
|
||||
Exec=dungeoncraft
|
||||
Icon=dungeoncraft
|
||||
Categories=Game;StrategyGame;
|
||||
Terminal=false
|
||||
"""
|
||||
|
||||
INFO_PLIST = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>DungeonCraft</string>
|
||||
<key>CFBundleDisplayName</key><string>DungeonCraft</string>
|
||||
<key>CFBundleIdentifier</key><string>org.familie-zink.dungeoncraft</string>
|
||||
<key>CFBundleExecutable</key><string>DungeonCraft</string>
|
||||
<key>CFBundleIconFile</key><string>DungeonCraft</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key><string>{short}</string>
|
||||
<key>CFBundleVersion</key><string>{version}</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>LSApplicationCategoryType</key><string>public.app-category.strategy-games</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"==> {msg}", flush=True)
|
||||
|
||||
|
||||
def run(cmd, env=None, cwd=ROOT):
|
||||
print(" $", " ".join(str(c) for c in cmd), flush=True)
|
||||
r = subprocess.run([str(c) for c in cmd], cwd=cwd, env=env)
|
||||
if r.returncode != 0:
|
||||
raise BuildError(f"{pathlib.Path(str(cmd[0])).name} failed (exit {r.returncode})")
|
||||
|
||||
|
||||
def version():
|
||||
return (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def cargo():
|
||||
c = shutil.which("cargo") or str(pathlib.Path.home() / ".cargo" / "bin" / "cargo")
|
||||
if not pathlib.Path(c).exists():
|
||||
raise BuildError("cargo not found: install Rust with rustup")
|
||||
return c
|
||||
|
||||
|
||||
def tool_env():
|
||||
env = dict(os.environ)
|
||||
env["PATH"] = str(pathlib.Path(cargo()).parent) + os.pathsep + env.get("PATH", "")
|
||||
return env
|
||||
|
||||
|
||||
def need_target(triple):
|
||||
rustup = shutil.which("rustup", path=tool_env()["PATH"])
|
||||
if not rustup:
|
||||
return
|
||||
have = subprocess.run([rustup, "target", "list", "--installed"], capture_output=True, text=True).stdout.split()
|
||||
if triple not in have:
|
||||
raise BuildError(f"Rust target missing: rustup target add {triple}")
|
||||
|
||||
|
||||
def zig_env():
|
||||
env = tool_env()
|
||||
if not shutil.which("cargo-zigbuild", path=env["PATH"]):
|
||||
raise BuildError("cargo-zigbuild missing: cargo install --locked cargo-zigbuild")
|
||||
installs = pathlib.Path.home() / ".local" / "share" / "mise" / "installs" / "zig"
|
||||
from_mise = sorted(installs.glob("*/zig"), key=lambda p: [int(x) if x.isdigit() else 0 for x in p.parent.name.split(".")])
|
||||
candidates = [env.get("CARGO_ZIGBUILD_ZIG_PATH"), shutil.which("zig", path=env["PATH"])] + [str(p) for p in reversed(from_mise)]
|
||||
|
||||
def works(z): # a mise shim without an active version is on PATH but does not run
|
||||
try:
|
||||
return subprocess.run([z, "version"], capture_output=True, timeout=30).returncode == 0
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
zig = next((z for z in candidates if z and works(z)), None)
|
||||
if not zig:
|
||||
raise BuildError("zig not found: install it (package manager, or `mise install zig@0.15`)")
|
||||
env["CARGO_ZIGBUILD_ZIG_PATH"] = zig
|
||||
return env
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Linux: AppImage
|
||||
|
||||
def appimagetool():
|
||||
tool = CACHE / "appimagetool-x86_64.AppImage"
|
||||
if not tool.exists():
|
||||
log(f"downloading appimagetool to {tool}")
|
||||
CACHE.mkdir(parents=True, exist_ok=True)
|
||||
tmp = tool.with_suffix(".part")
|
||||
with urllib.request.urlopen(APPIMAGETOOL_URL, timeout=120) as r, open(tmp, "wb") as f:
|
||||
shutil.copyfileobj(r, f)
|
||||
tmp.rename(tool)
|
||||
tool.chmod(tool.stat().st_mode | stat.S_IXUSR)
|
||||
return tool
|
||||
|
||||
|
||||
def build_linux(ver):
|
||||
if platform.system() != "Linux" or platform.machine() not in ("x86_64", "AMD64"):
|
||||
raise BuildError("the AppImage is built on x86_64 Linux")
|
||||
need_target("x86_64-unknown-linux-gnu")
|
||||
env = zig_env()
|
||||
# libasound is only linked by name: its own glibc references are the user's system's business
|
||||
env["RUSTFLAGS"] = (env.get("RUSTFLAGS", "") + " -C link-arg=-Wl,--allow-shlib-undefined").strip()
|
||||
log(f"Linux build (glibc {GLIBC})")
|
||||
run([cargo(), "zigbuild", "--release", "--target", f"x86_64-unknown-linux-gnu.{GLIBC}"], env=env)
|
||||
binary = ROOT / "target" / "x86_64-unknown-linux-gnu" / "release" / "dungeoncraft"
|
||||
|
||||
appdir = ROOT / "target" / "AppDir"
|
||||
shutil.rmtree(appdir, ignore_errors=True)
|
||||
(appdir / "usr" / "bin").mkdir(parents=True)
|
||||
shutil.copy2(binary, appdir / "usr" / "bin" / "dungeoncraft")
|
||||
(appdir / "dungeoncraft.desktop").write_text(DESKTOP_ENTRY, encoding="utf-8")
|
||||
shutil.copy2(ICON, appdir / "dungeoncraft.png")
|
||||
os.symlink("dungeoncraft.png", appdir / ".DirIcon")
|
||||
os.symlink("usr/bin/dungeoncraft", appdir / "AppRun")
|
||||
|
||||
out = DIST / f"DungeonCraft-{ver}-x86_64.AppImage"
|
||||
out.unlink(missing_ok=True)
|
||||
tenv = dict(os.environ, ARCH="x86_64", APPIMAGE_EXTRACT_AND_RUN="1") # no FUSE needed for the tool
|
||||
log("packing the AppImage")
|
||||
run([appimagetool(), "--no-appstream", appdir, out], env=tenv)
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ Windows: one exe
|
||||
|
||||
def build_windows(ver):
|
||||
need_target("x86_64-pc-windows-gnu")
|
||||
env = zig_env()
|
||||
log("Windows build")
|
||||
run([cargo(), "zigbuild", "--release", "--target", "x86_64-pc-windows-gnu"], env=env)
|
||||
out = DIST / f"DungeonCraft-{ver}-windows-x64.exe"
|
||||
shutil.copy2(ROOT / "target" / "x86_64-pc-windows-gnu" / "release" / "dungeoncraft.exe", out)
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ macOS: universal .app in a zip
|
||||
|
||||
def build_macos(ver):
|
||||
if platform.system() != "Darwin":
|
||||
raise BuildError("the macOS app can only be built on a Mac (Apple's SDK is needed)")
|
||||
env = tool_env()
|
||||
arches = ["aarch64-apple-darwin", "x86_64-apple-darwin"]
|
||||
for t in arches:
|
||||
need_target(t)
|
||||
env["MACOSX_DEPLOYMENT_TARGET"] = "11.0"
|
||||
for t in arches:
|
||||
log(f"macOS build {t}")
|
||||
run([cargo(), "build", "--release", "--target", t], env=env)
|
||||
|
||||
app = ROOT / "target" / "DungeonCraft.app"
|
||||
shutil.rmtree(app, ignore_errors=True)
|
||||
(app / "Contents" / "MacOS").mkdir(parents=True)
|
||||
(app / "Contents" / "Resources").mkdir(parents=True)
|
||||
run(["lipo", "-create", "-output", app / "Contents" / "MacOS" / "DungeonCraft"] + [ROOT / "target" / t / "release" / "dungeoncraft" for t in arches])
|
||||
short = ver.lstrip("v").replace("-", ".")
|
||||
(app / "Contents" / "Info.plist").write_text(INFO_PLIST.format(short=short, version=ver), encoding="utf-8")
|
||||
|
||||
iconset = ROOT / "target" / "DungeonCraft.iconset"
|
||||
shutil.rmtree(iconset, ignore_errors=True)
|
||||
iconset.mkdir()
|
||||
for size in (16, 32, 128, 256, 512):
|
||||
run(["sips", "-z", size, size, ICON, "--out", iconset / f"icon_{size}x{size}.png"])
|
||||
if size <= 256:
|
||||
run(["sips", "-z", size * 2, size * 2, ICON, "--out", iconset / f"icon_{size}x{size}@2x.png"])
|
||||
run(["iconutil", "-c", "icns", iconset, "-o", app / "Contents" / "Resources" / "DungeonCraft.icns"])
|
||||
|
||||
run(["codesign", "--force", "--deep", "--sign", "-", app])
|
||||
out = DIST / f"DungeonCraft-{ver}-macos.zip"
|
||||
out.unlink(missing_ok=True)
|
||||
run(["ditto", "-c", "-k", "--keepParent", app, out])
|
||||
return out
|
||||
|
||||
|
||||
BUILDERS = {"linux": build_linux, "windows": build_windows, "macos": build_macos}
|
||||
|
||||
|
||||
def main(argv):
|
||||
if any(a in ("-h", "--help") for a in argv):
|
||||
print(__doc__)
|
||||
return 0
|
||||
wanted = argv or (["macos"] if platform.system() == "Darwin" else ["linux", "windows"])
|
||||
unknown = [w for w in wanted if w not in BUILDERS]
|
||||
if unknown:
|
||||
print(f"unknown target(s): {', '.join(unknown)} (linux, windows, macos)", file=sys.stderr)
|
||||
return 2
|
||||
DIST.mkdir(exist_ok=True)
|
||||
ver = version()
|
||||
done, failed = [], []
|
||||
for w in wanted:
|
||||
try:
|
||||
done.append(BUILDERS[w](ver))
|
||||
except BuildError as e:
|
||||
failed.append(f"{w}: {e}")
|
||||
print(f"!! {w}: {e}", file=sys.stderr)
|
||||
print()
|
||||
for p in done:
|
||||
print(f"built {p.relative_to(ROOT)} ({p.stat().st_size / 1e6:.1f} MB)")
|
||||
for f in failed:
|
||||
print(f"failed {f}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in new issue
Block a user