Compare commits
49
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.
|
||||
|
||||
+21
-6
@@ -125,9 +125,24 @@ grid), guard and attack markers (shields and swords), savegames with the game st
|
||||
claim, mine and set torches; rooms by plan, traps, towers, research; the attack flag, shields and swords;
|
||||
strength by the difficulty.
|
||||
2. **Mouse cursor** (done, v202611-00053) - clearer hands for the hand tool (open, holding, slap, punch).
|
||||
3. **New heroes** - thief (steals gold, from wave 3), monk (immune to spells, 5), ranger (lays traps, 7),
|
||||
inquisitor (converts the weakest creatures back, 9).
|
||||
4. **Master talents** - Builder, Warlord, Warlock, Merchant (5 levels each), a toolbar button; per game,
|
||||
carried over in the campaign.
|
||||
5. **Weather above ground** - rain (puts out fire), fog (less sight), storms (lightning in trees and units).
|
||||
6. **Named creatures** - names per kind and language, epithets at higher levels, memorial stones.
|
||||
3. **New heroes** (done, v202611-00063, rogues.rs) - thief (sneaks to a treasury, steals up to 250 gold and runs
|
||||
to the gate; slain, his loot is yours; from wave 3), monk (spells do not touch him, 5), ranger (shoots, lays
|
||||
snares that hurt and stun a creature, 7), inquisitor (fragile, every 20 s turns a nearby kobold or spider to
|
||||
the heroes, 9). In the campaign from missions 5, 9, 12 and 16 on.
|
||||
4. **Master talents** (done, v202611-00064, talents.rs) - experience from slain heroes, digging, rooms, walls,
|
||||
traps, towers, spells and gold; points get dearer (300, 450, 600 ...). Builder (imps dig +10%, rooms -6% per
|
||||
level), Warlord (creatures +5% damage, -5% damage taken), Warlock (mana +15%, spells -6%), Merchant (wages
|
||||
-6%, treasure +6%); 5 levels each. The "Master" button (number of free points, pulsing) opens the window.
|
||||
AI masters spend their points themselves; per game, carried over in the campaign and co-op runs.
|
||||
5. **Weather above ground** (done, v202611-00066, weather.rs) - about 60% fair, 20% rain, 12% fog, 8% storm,
|
||||
minutes each with soft 20 s changes, only on maps with land above ground. Rain (and storms) put out burning
|
||||
bridges and keep fire from catching; fog halves the sight of units outside; storms strike units (40 damage)
|
||||
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 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).
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Brána se hroutí! Objevuje se strážce brány s obránci ({0})
|
||||
msg.gate_defenders = Brána povolává obránce ({0})!
|
||||
msg.level_up = {0} dosahuje úrovně {1}.
|
||||
msg.hero_slain = Poražen: {0}! Kořist: {1} zlata.
|
||||
msg.thief_in_treasury = V tvé pokladnici je zloděj!
|
||||
msg.thief_escaped = Zloděj utekl s {0} zlata!
|
||||
msg.snare_caught = Do pasti šlápl: {0}!
|
||||
msg.converted = Inkvizitor obrátil jednoho z tvých tvorů: {0}!
|
||||
msg.creature_died = Padl tvůj tvor: {0}.
|
||||
msg.portal_claimed = Portál je tvůj! Přitahuje tvory.
|
||||
msg.flag_removed = Útočná vlajka odstraněna. Tví tvorové se vracejí.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Nedostatek many (potřeba {0}).
|
||||
hint.portal_no_lair = Portál je otevřený, ale bez doupěte žádní tvorové nepřijdou.
|
||||
hint.no_farm = Tvorové chtějí jíst: postav houbovou zahradu.
|
||||
hint.lair_full = Všechna lůžka v doupěti jsou obsazená. Zvětši doupě.
|
||||
hint.creature_max = Tvá kobka má 100 tvorů - portálem už další nepřijdou.
|
||||
hint.gate_attack = Tví tvorové útočí na bránu hrdinů!
|
||||
hint.treasury_full = Tvé pokladnice jsou plné! Postav další políčka pokladnice.
|
||||
hint.flag_unreachable = Tví tvorové se k útočné vlajce nedostanou.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Pravé kliknutí Položit jednotku (zajatce i do mučírny)
|
||||
help.k_slap = Shift+Levé Ruka: pohlavek vlastním, pěst hrdinům na tvém území
|
||||
help.k_pan = Pravé tažení Posun mapy (také WASD/šipky, okraj obrazovky)
|
||||
help.k_rot = Prostřední tah. Otáčení a naklánění kamery (také Q/E, R/F)
|
||||
help.k_reset = Prostřední 2x klik Obnovit úhel a otočení (místo zůstane)
|
||||
help.k_zoom = Kolečko myši Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Volba nástroje
|
||||
help.k_misc = P / Mezerník Pauza M Zvuk zap./vyp. +/- Hlasitost
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Co do ní spadne, shoří. Přecházejí ji kamenné mosty.
|
||||
tile.bridge = Dřevěný most
|
||||
tile.lavabridge = Kamenný most
|
||||
tile.mushroom = Obří houba
|
||||
tile.mushroom_grown = Dorostlá: připravená ke kácení.
|
||||
tile.mushroom_growing = Roste: {0}% - pokácet ji lze až dorostlou.
|
||||
tile.grove = Houbový háj
|
||||
tile.grove_info = Tento mech nikdo nedobude. Obří houby tu znovu dorůstají.
|
||||
tile.tree = Strom
|
||||
tile.plant_info = Označ krumpáčem: skřítek ho pokácí za {0} dřeva.
|
||||
tile.drawbridge = Padací most
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Otevřené zápasy
|
||||
net.no_rooms = Žádné otevřené zápasy.
|
||||
net.create = Vytvořit zápas
|
||||
net.map_size = Mapa {0}x{1} · {2} hráči
|
||||
net.coop_mission = Kooperativní kampaň: mise {0} ({1})
|
||||
net.coop_create = Kooperace
|
||||
net.coop_editor = Kampaň z editoru
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = léčí se o třetinu poškození, které způsobí.
|
||||
unit.giant = Obr
|
||||
unit.fairy = Vílí špeh
|
||||
unit.battlemage = Bitevní mág
|
||||
unit.thief = Zloděj
|
||||
unit.monk = Mnich
|
||||
unit.ranger = Hraničář
|
||||
unit.inquisitor = Inkvizitor
|
||||
unit.ram = Beranidlo
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Hrdinové zničili past: {0}.
|
||||
best.giant.text = Obr je to, co se stane, když rytíř tři sta let jí zeleninu. Pomalý, obrovský a dveře bere jako pouhý návrh. | Pasti ničí už tím, že stojí vedle nich. Odporné způsoby. Bij ho vším, co máš, nejlépe z dálky.
|
||||
best.fairy.text = Malý udavač Řádu: zářící komár s křídly, poslaný napřed, aby našel tvé pasti a všem to vykecal. Jakmile past uvidí, hrdinové ji rozbijí. | Navíc sype na hrdiny léčivý prášek. Plácni ji první. Dvakrát, pro jistotu.
|
||||
best.battlemage.text = Mág v rouchu Řádu, který považuje zdi za osobní urážku. Zpevněné zdi pod jeho ohnivými koulemi praskají a on o tom nepřestane mluvit. | Zblízka křehký. Nejvíc ho štvou ocelové zdi - dobře.
|
||||
best.thief.text = Rychlé prsty, ještě rychlejší nohy. Nepřichází bojovat: vplíží se do tvé pokladnice, naplní si pytel a utíká zpátky k bráně. | Zabij ho dřív, než uteče, a zlato je zase tvoje - i s úroky.
|
||||
best.monk.text = Roky půstu a modliteb ho učinily hluchým k tvé magii: blesk, mor i mráz z něj stékají jako déšť ze střechy. | Pěsti taky bolí. Přivítej ho ocelí, drápy a zuby.
|
||||
best.ranger.text = Lesník Řádu, který střílí z dálky a nastražuje v tvých chodbách železné pasti. Tvor, který do jedné šlápne, je zraněn a chvíli uvízne. | Pasti zůstávají, dokud do nich někdo nešlápne. Raději kobold než tvůj zlobr.
|
||||
best.inquisitor.text = Křehký, ale jeho kázání jsou nebezpečná: každých dvacet sekund přemluví jednoho z tvých nejslabších tvorů - kobolda nebo pavouka -, aby přešel k hrdinům. | Silní tvorové se mu jen vysmějí. Udeř první.
|
||||
best.ram.text = Kmen s železnou hlavicí a stříškou proti šípům. Tvé bytosti ho nezajímají – tvé dveře, zdi a věže ano. | Zastav ho před dveřmi, nebo si sežeň nové.
|
||||
best.catapult.text = Dřevěné rameno, vědro kamení a rytíř s příliš mnoha volnými chvílemi. Střílí dál než tvé věže a míří nejprve na ně. | Vrhni se na něj: zblízka neublíží ani mouše.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Rubínová žíla
|
||||
ed.terrain.diamond = Diamantová žíla
|
||||
ed.terrain.steel = Ocelová zeď
|
||||
ed.terrain.mushroom = Obří houba
|
||||
ed.terrain.grove = Houbový háj
|
||||
ed.terrain.grass = Tráva (povrch)
|
||||
ed.terrain.tree = Strom
|
||||
ed.place.core = Jádro duše
|
||||
@@ -1354,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ěč
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Křivoprst: Ha! Slyšel jsi to? Něco přichází od východu. Připr
|
||||
tut.obj6 = Poraz dva rytíře.
|
||||
tut.knights = Křivoprst: Dva rytíři! Pošli na ně tvory útočnou vlajkou (Rozkazy) nebo je zasáhni bleskem.
|
||||
tut.win = Křivoprst: Výborně, pane! To je všechno. No - skoro. Zbytek poví kampaň. Přistavím vodu na čaj.
|
||||
|
||||
# master talents
|
||||
cat.master = Pán
|
||||
talent.title = Pánovy talenty
|
||||
talent.points = Volné body talentů: {0}
|
||||
talent.next = {0}/{1} zkušeností do dalšího bodu
|
||||
talent.all = Všechny talenty jsou naučené.
|
||||
talent.builder = Stavitel
|
||||
talent.warlord = Vojevůdce
|
||||
talent.warlock = Černokněžník
|
||||
talent.merchant = Obchodník
|
||||
talent.builder.desc = Čertíci kopou o {0}% rychleji, místnosti stojí o {1}% méně.
|
||||
talent.warlord.desc = Tvorové udeří o {0}% silněji a dostávají o {1}% méně poškození.
|
||||
talent.warlock.desc = Mana proudí o {0}% rychleji, kouzla stojí o {1}% méně.
|
||||
talent.merchant.desc = Mzdy jsou o {0}% nižší, doručené poklady mají o {1}% vyšší hodnotu.
|
||||
talent.none = Zatím nenaučeno.
|
||||
talent.then = Další úroveň: {0}
|
||||
msg.talent_point = Nový bod talentu! Použij ho v okně pána.
|
||||
msg.talent_learned = {0}: úroveň {1} naučena.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Jasno
|
||||
weather.rain = Déšť
|
||||
weather.fog = Mlha
|
||||
weather.storm = Bouře
|
||||
msg.weather_clear = Na povrchu se vyjasňuje.
|
||||
msg.weather_rain = Na povrchu začíná pršet - oheň nechytne.
|
||||
msg.weather_fog = Nad krajinou se valí mlha - venku každý vidí méně.
|
||||
msg.weather_storm = Na povrchu se blíží bouře! Blesky zasahují jednotky a stromy.
|
||||
msg.lightning_unit = Zásah bleskem: {0}!
|
||||
# creature names
|
||||
names.imp = Drobek, Soplík, Hrabík, Bláťák, Špindíra, Prcek, Rýpal, Čuník, Smradík, Škrabka, Hrouda, Čvachta, Mazal, Flekáč, Hrudka, Lopatka, Čumák, Drobeček, Šmudla, Čmudla
|
||||
names.kobold = Ňafík, Štípal, Zubáč, Prašivec, Kousal, Rezák, Šmik, Kousek, Čumil, Krysák, Hadrník, Bodlák, Drápek, Rejpal, Kořínek, Chechtal, Strkal, Zoubek, Křup, Škrábek
|
||||
names.spider = Lezoun, Chlupka, Pavoučík, Lechtal, Síťka, Osminožka, Nitka, Strašidlík, Šmatlík, Hedvábka, Tkadlenka, Skokan, Chlupáček, Tekla, Cívka, Chňapka, Klubíčko, Lepka, Vrtílek, Jehlička
|
||||
names.mage = Bartoloměj, Ignác, Leopold, Theobald, Vojtěch, Jeroným, Valdemar, Gotfríd, Kornelius, Otakar, Florián, Engelbert, Pankrác, Ruprecht, Konstantin, Eusebius, Servác, Barnabáš, Archibald, Bonifác
|
||||
names.ogre = Hromotluk, Špalek, Bum, Balvan, Tupoun, Pařez, Bouchal, Mlátič, Dupák, Dřevák, Žbluňk, Buřtík, Valoun, Břicháč, Kyjan, Vůl, Rachot, Kus, Buben, Gorbo
|
||||
names.jailer = Zachmuřenec, Závora, Okov, Zámek, Žalář, Železovous, Kroužek, Šroub, Přísný, Mřížník, Mrzout, Hák, Oprátka, Klíčník, Temný, Řinčal, Kláda, Strážný, Klec, Hořký
|
||||
names.priestess = Žhavka, Plamena, Popelice, Serafína, Vatra, Ohnivka, Morana, Brunhilda, Ignie, Jiskra, Šarlata, Žárka, Havranka, Lilit, Troud, Izolda, Planoucí, Nyx, Večernice, Plaménka
|
||||
names.chainbreaker = Řetězolam, Ulfgar, Železoduch, Brannok, Kragmor, Valdrek, Trn, Gorath, Nespoutaný, Rurik, Torvald, Hagen, Durnok, Střep, Plamen, Vlkoslav, Korrun, Arvak, Osvoboditel, Halvard
|
||||
names.bat = Pískal, Třepetal, Pípa, Šustík, Ozvěna, Mávalka, Soumrak, Pískálek, Cvrlík, Zoubek, Vír, Netopýrek, Mol, Pípálek, Šuměl, Visálek, Samet, Šmik, Kvičal, Blesk
|
||||
names.gargoyle = Žula, Břidlice, Čedič, Kameník, Malta, Oblázek, Štěrk, Pazourek, Spára, Ruina, Křemen, Pemza, Římsa, Suk, Kamenovous, Bručoun, Štít, Chrlič, Dláto, Sokl
|
||||
names.owl = Výr, Chytrolín, Brk, Bartold, Minerva, Tobiáš, Hůhů, Sovička, Pírko, Povzdech, Myslitel, Stařešina, Kulíšek, Noctua, Sovoslav, Pergamen, Šero, Puštík, Profesor, Prachovka
|
||||
names.wyrm = Vermitrax, Popelochřtán, Spálenec, Kalderon, Ignivar, Žhnoroh, Drakkar, Pyrrhos, Obsidián, Žhavochřtán, Vulkanor, Pálizub, Malgrath, Tyrax, Inferno, Síra, Kaldor, Fafnir, Ragnor, Uhlík
|
||||
names.cogling = Ozubák, Kolečko, Šroubek, Řehtačka, Nýtek, Převod, Kutil, Udělátko, Klíček, Pastorek, Cinkal, Šťoural, Matička, Bzučák, Klička, Káča, Příruba, Cvak, Tiktak, Vrut
|
||||
names.revenant = Stesk, Hořkost, Popelavý, Sten, Prázdno, Žal, Bledý, Trud, Stín, Prach, Hrobník, Soumrak, Krypta, Tesknota, Bdění, Mohyla, Rubáš, Elegie, Kal, Pustina
|
||||
names.beast = Chlupáč, Míša, Bruno, Huňáč, Mlaskal, Mechoun, Bručal, Medvídek, Křoví, Chroup, Cvakal, Lezec, Štětinka, Mech, Burácel, Ostružina, Cvrkal, Brblal, Brumla, Čmuchal
|
||||
names.human = Gerhard, Roland, Konrád, Albrecht, Tristan, Eckhart, Gawain, Lancelot, Parsifal, Hugo, Bertram, Gotfríd, Ortvin, Balduin, Rajnold, Vilfríd, Dětřich, Gilbert, Ruprecht, Bořivoj
|
||||
epithets.mid = Kostilam, Blatonoh, Krysokous, Špinoxicht, Kamenožrout, Lebkolam, Smradopěst, Rezokloub, Břichobij, Holenokop, Plísňovous, Špinodráp, Soplomet, Uchokous, Kolenolam, Červojed
|
||||
epithets.high = Královrah, Zhoubopěst, Hrůzozub, Světoborec, Hrdinobij, Bouřepěst, Můrovládce, Duševrah, Železosrdce, Hradoborec, Drakobijce, Pekelník, Stínovládce, Krvokorunec, Zhoubotok, Hvězdolam
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Porten smuldrer! Portvogteren dukker op med {0} forsvarere!
|
||||
msg.gate_defenders = Porten tilkalder {0} forsvarere!
|
||||
msg.level_up = {0} når niveau {1}.
|
||||
msg.hero_slain = {0} besejret! Bytte: {1} guld.
|
||||
msg.thief_in_treasury = En tyv er i dit skatkammer!
|
||||
msg.thief_escaped = En tyv slap væk med {0} guld!
|
||||
msg.snare_caught = Trådt i en saks: {0}!
|
||||
msg.converted = Inkvisitoren har omvendt et af dine væsner: {0}!
|
||||
msg.creature_died = Din {0} er faldet.
|
||||
msg.portal_claimed = Portalen er din! Den tiltrækker væsner.
|
||||
msg.flag_removed = Angrebsflaget er fjernet. Dine væsner vender tilbage.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Ikke nok mana ({0} kræves).
|
||||
hint.portal_no_lair = Portalen er åben, men uden et hi kommer der ingen væsner.
|
||||
hint.no_farm = Væsner vil have mad: byg en svampehave.
|
||||
hint.lair_full = Alle senge i hiet er optaget. Gør dit hi større.
|
||||
hint.creature_max = Dit fangehul huser 100 væsner - der kommer ikke flere gennem portalen.
|
||||
hint.gate_attack = Dine væsner angriber helteporten!
|
||||
hint.treasury_full = Dine skatkamre er fulde! Byg flere skatkammerfelter.
|
||||
hint.flag_unreachable = Dine væsner kan ikke nå angrebsflaget.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Højreklik Sæt en enhed ned (fanger: også i torturkammere
|
||||
help.k_slap = Shift+Venstre Hånd: bagkast til egne enheder, slå helte på din jord
|
||||
help.k_pan = Højre træk Flyt kortet (også WASD/pile, skærmkant)
|
||||
help.k_rot = Midter træk Drej og vip kameraet (også Q/E, R/F)
|
||||
help.k_reset = Midter 2x klik Nulstil vinkel og drejning (stedet bliver)
|
||||
help.k_zoom = Musehjul Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Vælg værktøj
|
||||
help.k_misc = P / Mellemrum Pause M Lyd til/fra +/- Lydstyrke
|
||||
@@ -258,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
|
||||
@@ -459,6 +467,10 @@ tile.lava_info = Hvad der falder i, brænder. Stenbroer fører over.
|
||||
tile.bridge = Træbro
|
||||
tile.lavabridge = Stenbro
|
||||
tile.mushroom = Kæmpesvamp
|
||||
tile.mushroom_grown = Fuldvoksen: klar til fældning.
|
||||
tile.mushroom_growing = Vokser: {0}% - kan først fældes, når den er fuldvoksen.
|
||||
tile.grove = Svampelund
|
||||
tile.grove_info = Ingen kan erobre denne mos. Her vokser kæmpesvampe igen.
|
||||
tile.tree = Træ
|
||||
tile.plant_info = Markér med hakken: en imp fælder den for {0} træ.
|
||||
tile.drawbridge = Vindebro
|
||||
@@ -723,6 +735,7 @@ net.error = {0}
|
||||
net.rooms = Åbne kampe
|
||||
net.no_rooms = Ingen åbne kampe.
|
||||
net.create = Opret kamp
|
||||
net.map_size = Kort {0}x{1} · {2} spillere
|
||||
net.coop_mission = Co-op-kampagne: mission {0} ({1})
|
||||
net.coop_create = Co-op-kampagne
|
||||
net.coop_editor = Editor-kampagne
|
||||
@@ -1082,12 +1095,20 @@ ability.drain.desc = heler sig selv med en tredjedel af den skade, den giver.
|
||||
unit.giant = Kæmpe
|
||||
unit.fairy = Feespion
|
||||
unit.battlemage = Kampmagiker
|
||||
unit.thief = Tyv
|
||||
unit.monk = Munk
|
||||
unit.ranger = Jæger
|
||||
unit.inquisitor = Inkvisitor
|
||||
unit.ram = Rambuk
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Heltene ødelagde en fælde: {0}.
|
||||
best.giant.text = En kæmpe er, hvad der sker, når en ridder spiser sine grøntsager i tre hundrede år. Langsom, enorm, og den betragter døre som et forslag. | Den ødelægger fælder bare ved at stå ved siden af dem. Væmmelige manerer. Ram den med alt, helst på afstand.
|
||||
best.fairy.text = Ordenens lille stikker: en glødende myg med vinger, sendt i forvejen for at finde dine fælder og fortælle alle om dem. Når den har set en fælde, smadrer heltene den. | Den drysser også helende støv på heltene. Smæk den først. To gange, for en sikkerheds skyld.
|
||||
best.battlemage.text = En magiker i Ordenens kåbe, der opfatter mure som en personlig fornærmelse. Forstærkede mure brister under hans ildkugler, og han holder aldrig op med at tale om det. | Skrøbelig på nært hold. Stålmure irriterer ham mest - godt.
|
||||
best.thief.text = Hurtige fingre, endnu hurtigere fødder. Han kommer ikke for at kæmpe: han sniger sig ind i dit skatkammer, fylder sin sæk og løber tilbage til porten. | Dræb ham, før han slipper væk, og guldet er dit igen - med renter.
|
||||
best.monk.text = Års faste og bøn har gjort ham døv for din magi: lyn, pest og frost preller af på ham som regn på et tag. | Knytnæver gør også ondt. Mød ham med stål, kløer og tænder.
|
||||
best.ranger.text = En skovmand fra Ordenen, der skyder på afstand og lægger jernsakse ud i dine gange. Et væsen, der træder i en, bliver såret og sidder fast et stykke tid. | Saksene bliver liggende, til nogen træder i dem. Hellere en kobold end din trold.
|
||||
best.inquisitor.text = Skrøbelig, men hans prædikener er farlige: hvert tyvende sekund taler han et af dine svageste væsner - en kobold eller en edderkop - over på heltenes side. | Stærke væsner griner bare af ham. Slå til først.
|
||||
best.ram.text = En træstamme med jernhætte og et tag mod pile. Dine væsner er ligegyldige for den – dine døre, mure og tårne ikke. | Stop den, før den når døren, eller skaf en ny dør.
|
||||
best.catapult.text = En træarm, en spand sten og en ridder med for meget fritid. Den rækker længere end dine tårne og sigter efter dem først. | Storm frem: på nært hold gør den ikke en flue fortræd.
|
||||
|
||||
@@ -1271,6 +1292,7 @@ ed.terrain.ruby = Rubinåre
|
||||
ed.terrain.diamond = Diamantåre
|
||||
ed.terrain.steel = Stålmur
|
||||
ed.terrain.mushroom = Kæmpesvamp
|
||||
ed.terrain.grove = Svampelund
|
||||
ed.terrain.grass = Græs (over jorden)
|
||||
ed.terrain.tree = Træ
|
||||
ed.place.core = Sjælekerne
|
||||
@@ -1353,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
|
||||
@@ -1372,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.
|
||||
@@ -1394,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
|
||||
@@ -1442,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).
|
||||
@@ -1672,3 +1700,70 @@ tut.slap = Krumfinger: Ha! Hørte du det? Noget kommer fra øst. Gør dig klar:
|
||||
tut.obj6 = Besejr de to riddere.
|
||||
tut.knights = Krumfinger: To riddere! Send dine væsner af sted med angrebsflaget (Ordrer), eller ram dem med lynet.
|
||||
tut.win = Krumfinger: Godt gået, mester! Det er alt, hvad der skal til. Altså – næsten. Kampagnen fortæller resten. Jeg sætter kedlen over.
|
||||
|
||||
# master talents
|
||||
cat.master = Mester
|
||||
talent.title = Mestertalenter
|
||||
talent.points = Frie talentpoint: {0}
|
||||
talent.next = {0}/{1} erfaring til næste point
|
||||
talent.all = Alle talenter er lært.
|
||||
talent.builder = Bygmester
|
||||
talent.warlord = Krigsherre
|
||||
talent.warlock = Heksemester
|
||||
talent.merchant = Købmand
|
||||
talent.builder.desc = Imps graver {0}% hurtigere, rum koster {1}% mindre.
|
||||
talent.warlord.desc = Væsner slår {0}% hårdere og tager {1}% mindre skade.
|
||||
talent.warlock.desc = Mana flyder {0}% hurtigere, besværgelser koster {1}% mindre.
|
||||
talent.merchant.desc = Lønninger er {0}% lavere, afleverede skatte er {1}% mere værd.
|
||||
talent.none = Endnu ikke lært.
|
||||
talent.then = Næste niveau: {0}
|
||||
msg.talent_point = Et nyt talentpoint! Brug det i mestervinduet.
|
||||
msg.talent_learned = {0} niveau {1} lært.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Klart
|
||||
weather.rain = Regn
|
||||
weather.fog = Tåge
|
||||
weather.storm = Storm
|
||||
msg.weather_clear = Over jorden klarer det op.
|
||||
msg.weather_rain = Over jorden begynder det at regne - ild fænger ikke.
|
||||
msg.weather_fog = Tåge driver ind over landet - udenfor ser alle mindre.
|
||||
msg.weather_storm = Over jorden trækker en storm op! Lynet rammer enheder og træer.
|
||||
msg.lightning_unit = Ramt af lynet: {0}!
|
||||
# creature names
|
||||
names.imp = Krumme, Snotte, Graver, Mudder, Fnug, Pjok, Rodemus, Snavs, Bussemand, Kradser, Brokke, Splatter, Søle, Pirk, Knold, Skovl, Snude, Pusling, Plet, Grums
|
||||
names.kobold = Glam, Knib, Griberen, Skurv, Hap, Rusten, Svup, Bidder, Gab, Rotte, Flænge, Pigge, Klo, Ballade, Rod, Fnis, Skubber, Hak, Knæk, Skramme
|
||||
names.spider = Kravle, Uldben, Edderkop, Kilde, Netto, Ottefod, Tråde, Gyselig, Fimre, Silke, Væverske, Hopsa, Fnugben, Tekla, Spole, Snappe, Garnnøgle, Klistre, Vimse, Nålen
|
||||
names.mage = Bartholomæus, Ignatius, Leopold, Theobald, Adalbert, Hieronymus, Valdemar, Gotfred, Cornelius, Ottokar, Florentin, Engelbrecht, Pankratius, Rupertus, Konstantin, Eusebius, Severin, Barnabas, Archibald, Vitus
|
||||
names.ogre = Brum, Klods, Bums, Klump, Dunk, Tumpe, Bøvl, Klasker, Tramp, Træhoved, Plump, Mops, Bolle, Vom, Knippel, Okse, Rumle, Klat, Hump, Gorbo
|
||||
names.jailer = Grum, Slå, Lænke, Lås, Fangekælder, Jernskæg, Bøjle, Bolt, Streng, Tremme, Gnaven, Krog, Strikke, Nøgle, Dyster, Klirr, Stokmand, Slutter, Bur, Bitter
|
||||
names.priestess = Glød, Flamme, Aske, Serafina, Lue, Ildine, Morwen, Brynhild, Ignia, Gnist, Skarlagen, Funke, Ravna, Lilith, Tønder, Isolde, Brandhild, Nyx, Vesper, Glimt
|
||||
names.chainbreaker = Lænkebryder, Ulfgar, Jernsjæl, Brannok, Kragmor, Valdrek, Torn, Gorath, Ubunden, Rurik, Torvald, Hagen, Durnok, Splint, Brand, Ulfrik, Korrun, Arvak, Sigurd, Halvard
|
||||
names.bat = Pip, Flagre, Piber, Susle, Ekko, Flap, Tusmørke, Hvisle, Kvidre, Tandsen, Hvirvel, Flagermus, Møl, Pippe, Suse, Dingle, Fløjl, Smutte, Hvin, Lynet
|
||||
names.gargoyle = Granit, Skifer, Basalt, Grimbert, Mørtel, Kisel, Skærver, Flint, Fuge, Murbrok, Kvarts, Pimpsten, Gesims, Knort, Stenskæg, Gnaven, Gavl, Vandspyer, Mejsel, Sokkel
|
||||
names.owl = Uhu, Klogeåge, Fjerpen, Bartholdus, Minerva, Tobias, Huhu, Uglalia, Fjerdrik, Suk, Grubler, Ældste, Kirkeugle, Noctua, Uglebert, Pergament, Skumring, Natugle, Professor, Dun
|
||||
names.wyrm = Vermithrax, Askegab, Svider, Calderon, Ignivar, Gløderhorn, Drakkar, Pyrrhus, Obsidian, Glødgab, Vulkanor, Brandtand, Malgrath, Tyrax, Inferno, Svovl, Kaldor, Fafner, Ragnor, Sinderos
|
||||
names.cogling = Tandhjul, Hjulet, Skrue, Skralde, Bolt, Gear, Fikser, Dimsedut, Nøglen, Drev, Klirre, Fedter, Nitte, Snurre, Krumtap, Snurretop, Flange, Klikklak, Tiktak, Nippel
|
||||
names.revenant = Morgen, Nag, Askegrå, Klage, Hul, Sorg, Bleg, Kummer, Skygge, Muld, Gravmand, Tusmørke, Krypt, Vemod, Vågen, Høj, Ligklæde, Elegi, Bærme, Øde
|
||||
names.beast = Pjusk, Brumbjørn, Bruno, Lurvet, Mumle, Mosryg, Brummer, Ursa, Knort, Knas, Klikker, Kravler, Børste, Mos, Rumlen, Tjørn, Knirke, Gnavpot, Bamse, Snuse
|
||||
names.human = Gerhard, Roland, Konrad, Albrecht, Tristan, Eskild, Gavin, Lancelot, Palle, Holger, Bertram, Gotfred, Esbern, Valdemar, Regnar, Vilfred, Ditlev, Gilbert, Rasmus, Dirk
|
||||
epithets.mid = Knoglebryder, Mudderfod, Rottebider, Skidtfjæs, Stengnaver, Kraniebanker, Stinkenæve, Rustknogle, Mavestøder, Skinnebensspark, Skimmelskæg, Skidtklo, Snotslynger, Øregnaver, Knæknækker, Ormeæder
|
||||
epithets.high = Kongedræber, Undergangshånd, Rædselstand, Verdensknuser, Heltebane, Stormnæve, Natterædsel, Sjælehøster, Jernhjerte, Borgknuser, Drageskræk, Helvedesvandrer, Skyggefyrste, Blodkrone, Dommeflod, Stjerneknuser
|
||||
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
|
||||
@@ -63,6 +63,10 @@ msg.gate_boss = Das Tor wankt! Der Torwächter erscheint mit {0} Verteidigern!
|
||||
msg.gate_defenders = Das Tor ruft {0} Verteidiger herbei!
|
||||
msg.level_up = {0} erreicht Stufe {1}.
|
||||
msg.hero_slain = {0} besiegt! Beute: {1} Gold.
|
||||
msg.thief_in_treasury = Ein Dieb ist in deiner Schatzkammer!
|
||||
msg.thief_escaped = Ein Dieb ist mit {0} Gold entkommen!
|
||||
msg.snare_caught = In eine Falle getreten: {0}!
|
||||
msg.converted = Der Inquisitor hat eine deiner Kreaturen bekehrt: {0}!
|
||||
msg.creature_died = Dein {0} ist gefallen.
|
||||
msg.portal_claimed = Das Portal gehört nun dir! Kreaturen werden davon angelockt.
|
||||
msg.flag_removed = Angriffsflagge entfernt. Deine Kreaturen kehren zurück.
|
||||
@@ -80,6 +84,7 @@ msg.no_mana_cost = Nicht genug Mana ({0} benötigt).
|
||||
hint.portal_no_lair = Das Portal ist offen, aber ohne Quartier kommen keine Kreaturen.
|
||||
hint.no_farm = Kreaturen wollen essen: Baue einen Pilzgarten.
|
||||
hint.lair_full = Alle Quartierplätze sind belegt. Vergrößere dein Quartier.
|
||||
hint.creature_max = Dein Kerker beherbergt 100 Kreaturen - mehr kommen nicht durch das Portal.
|
||||
hint.gate_attack = Deine Kreaturen greifen das Heldentor an!
|
||||
hint.treasury_full = Deine Schatzkammern sind voll! Baue mehr Schatzkammer-Felder.
|
||||
hint.flag_unreachable = Die Angriffsflagge ist für deine Kreaturen nicht erreichbar.
|
||||
@@ -153,6 +158,7 @@ help.k_right = Rechtsklick Einheit absetzen (Gefangene auch in den Kerker)
|
||||
help.k_slap = Shift+Links Hand: Rückhand für eigene, Faust für Helden auf eigenem Gebiet
|
||||
help.k_pan = Rechts ziehen Karte verschieben (auch WASD/Pfeile, Bildrand)
|
||||
help.k_rot = Mitte ziehen Kamera drehen und neigen (auch Q/E, R/F)
|
||||
help.k_reset = Mitte 2x klicken Blickwinkel und Drehung zurücksetzen (Ort bleibt)
|
||||
help.k_zoom = Mausrad Zoomen
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Werkzeug
|
||||
help.k_misc = P / Leertaste Pause M Ton an/aus +/- Lautstärke
|
||||
@@ -257,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
|
||||
@@ -459,6 +467,10 @@ tile.lava_info = Was hineinfällt, verbrennt. Steinbrücken führen hinüber.
|
||||
tile.bridge = Holzbrücke
|
||||
tile.lavabridge = Steinbrücke
|
||||
tile.mushroom = Riesenpilz
|
||||
tile.mushroom_grown = Ausgewachsen: bereit zum Fällen.
|
||||
tile.mushroom_growing = Wächst: {0}% - erst ausgewachsen kann er gefällt werden.
|
||||
tile.grove = Pilzhain
|
||||
tile.grove_info = Dieses Moos kann niemand erobern. Hier wachsen Riesenpilze nach.
|
||||
tile.tree = Baum
|
||||
tile.plant_info = Mit der Spitzhacke markieren: Ein Imp fällt ihn für {0} Holz.
|
||||
tile.drawbridge = Zugbrücke
|
||||
@@ -723,6 +735,7 @@ net.error = {0}
|
||||
net.rooms = Offene Partien
|
||||
net.no_rooms = Keine offenen Partien.
|
||||
net.create = Partie erstellen
|
||||
net.map_size = Karte {0}x{1} · {2} Spieler
|
||||
net.coop_mission = Koop-Kampagne: Mission {0} ({1})
|
||||
net.coop_create = Koop-Kampagne
|
||||
net.coop_editor = Editor-Kampagne
|
||||
@@ -1082,12 +1095,20 @@ ability.drain.desc = heilt sich um ein Drittel des Schadens, den er austeilt.
|
||||
unit.giant = Riese
|
||||
unit.fairy = Feenspion
|
||||
unit.battlemage = Kampfmagier
|
||||
unit.thief = Dieb
|
||||
unit.monk = Mönch
|
||||
unit.ranger = Waldläufer
|
||||
unit.inquisitor = Inquisitor
|
||||
unit.ram = Rammbock
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Die Helden haben eine Falle zerstört: {0}.
|
||||
best.giant.text = Ein Riese ist, was passiert, wenn ein Ritter dreihundert Jahre lang sein Gemüse isst. Langsam, gewaltig, und Türen hält er für einen Vorschlag. | Fallen zertrümmert er, indem er einfach danebensteht. Widerliche Manieren. Bearbeite ihn mit allem, am besten aus der Ferne.
|
||||
best.fairy.text = Der kleine Spitzel des Ordens: eine leuchtende Mücke mit Flügeln, vorausgeschickt, um deine Fallen zu finden und es allen zu erzählen. Hat sie eine Falle gesehen, schlagen die Helden sie kaputt. | Außerdem streut sie Heilstaub auf die Helden. Erschlag sie zuerst. Zweimal, sicherheitshalber.
|
||||
best.battlemage.text = Ein Magier in der Robe des Ordens, der Mauern als persönliche Beleidigung empfindet. Verstärkte Wände zerplatzen unter seinen Feuerbällen, und er hört nicht auf, davon zu reden. | Aus der Nähe zerbrechlich. Stahlwände ärgern ihn am meisten - gut so.
|
||||
best.thief.text = Flinke Finger, noch flinkere Füße. Er kommt nicht zum Kämpfen: Er schleicht in deine Schatzkammer, füllt seinen Sack und rennt zurück zum Tor. | Erwisch ihn, bevor er entkommt, und das Gold gehört wieder dir - mit Zinsen.
|
||||
best.monk.text = Jahre des Fastens und Betens haben ihn taub für deine Magie gemacht: Blitz, Seuche und Frost perlen an ihm ab wie Regen am Dach. | Fäuste tun auch weh. Begegne ihm mit Stahl, Klauen und Zähnen.
|
||||
best.ranger.text = Ein Waldmann des Ordens, der aus der Ferne schießt und eiserne Fallen in deinen Gängen auslegt. Wer hineintritt, wird verletzt und steckt eine Weile fest. | Die Fallen bleiben, bis jemand hineintritt. Lieber ein Kobold als dein Oger.
|
||||
best.inquisitor.text = Gebrechlich, aber seine Predigten sind gefährlich: Alle zwanzig Sekunden redet er eine deiner schwächsten Kreaturen - einen Kobold oder eine Spinne - auf die Seite der Helden. | Starke Kreaturen lachen ihn nur aus. Schlag zuerst zu.
|
||||
best.ram.text = Ein Baumstamm mit Eisenkappe und einem Dach gegen Pfeile. Deine Kreaturen sind ihm egal – nur deine Türen, Wände und Türme nicht. | Halte ihn auf, bevor er die Tür erreicht, oder besorg dir eine neue.
|
||||
best.catapult.text = Ein Holzarm, ein Eimer Steine und ein Ritter mit zu viel Zeit. Es schießt weiter als deine Türme und nimmt sie zuerst ins Visier. | Stürm drauf los: aus der Nähe tut es keiner Fliege etwas.
|
||||
|
||||
@@ -1271,6 +1292,7 @@ ed.terrain.ruby = Rubinader
|
||||
ed.terrain.diamond = Diamantader
|
||||
ed.terrain.steel = Stahlwand
|
||||
ed.terrain.mushroom = Riesenpilz
|
||||
ed.terrain.grove = Pilzhain
|
||||
ed.terrain.grass = Gras (Oberwelt)
|
||||
ed.terrain.tree = Baum
|
||||
ed.place.core = Seelenkern
|
||||
@@ -1353,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
|
||||
@@ -1372,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.
|
||||
@@ -1394,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
|
||||
@@ -1442,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).
|
||||
@@ -1672,3 +1700,70 @@ tut.slap = Crookfinger: Ha! Hast du das gehört? Aus dem Osten kommt etwas. Mach
|
||||
tut.obj6 = Besiege die beiden Ritter.
|
||||
tut.knights = Crookfinger: Zwei Ritter! Schick deine Kreaturen mit der Angriffsflagge (Befehle) oder triff sie mit dem Blitz.
|
||||
tut.win = Crookfinger: Gut gemacht, Meister! Mehr ist es nicht. Na ja – fast. Den Rest erzählt die Kampagne. Ich setze Wasser auf.
|
||||
|
||||
# master talents
|
||||
cat.master = Meister
|
||||
talent.title = Meister-Talente
|
||||
talent.points = Freie Talentpunkte: {0}
|
||||
talent.next = {0}/{1} Erfahrung bis zum nächsten Punkt
|
||||
talent.all = Alle Talente sind gelernt.
|
||||
talent.builder = Baumeister
|
||||
talent.warlord = Kriegsherr
|
||||
talent.warlock = Hexer
|
||||
talent.merchant = Händler
|
||||
talent.builder.desc = Imps graben {0}% schneller, Räume kosten {1}% weniger.
|
||||
talent.warlord.desc = Kreaturen schlagen {0}% härter zu und nehmen {1}% weniger Schaden.
|
||||
talent.warlock.desc = Mana fließt {0}% schneller, Zauber kosten {1}% weniger.
|
||||
talent.merchant.desc = Löhne sind {0}% niedriger, gelieferte Schätze sind {1}% mehr wert.
|
||||
talent.none = Noch nicht gelernt.
|
||||
talent.then = Nächste Stufe: {0}
|
||||
msg.talent_point = Ein neuer Talentpunkt! Verteile ihn im Meister-Fenster.
|
||||
msg.talent_learned = {0} Stufe {1} gelernt.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Heiter
|
||||
weather.rain = Regen
|
||||
weather.fog = Nebel
|
||||
weather.storm = Sturm
|
||||
msg.weather_clear = Über der Erde klart es auf.
|
||||
msg.weather_rain = Über der Erde beginnt es zu regnen - Feuer fängt nicht.
|
||||
msg.weather_fog = Nebel zieht über das Land - draußen sieht jeder weniger.
|
||||
msg.weather_storm = Über der Erde zieht ein Sturm auf! Blitze treffen Einheiten und Bäume.
|
||||
msg.lightning_unit = Vom Blitz getroffen: {0}!
|
||||
# creature names
|
||||
names.imp = Krümel, Schnodder, Buddel, Matsch, Fussel, Knirps, Wühli, Schmutzi, Popel, Kratzer, Grummel, Bröckel, Zappel, Schlammi, Pfriem, Knubbel, Grabi, Rotzi, Wusel, Schaufel
|
||||
names.kobold = Kläff, Zwick, Raffzahn, Krätz, Schnapp, Rostig, Zack, Beißer, Klau, Ratz, Fetz, Zausel, Kralle, Stänker, Wurz, Fiesling, Rempel, Zinken, Knick, Schrammi
|
||||
names.spider = Krabbel, Wollbein, Spinnchen, Kitzel, Netti, Achtfuß, Fädchen, Gruselchen, Tüdel, Seidi, Webbi, Huschel, Krümelbein, Tekla, Spindel, Fangi, Zwirn, Klebi, Wuschel, Nadelchen
|
||||
names.mage = Bartholomäus, Ignatius, Leopold, Theobald, Adalbert, Kunibert, Wendelin, Gottlieb, Hieronymus, Ottokar, Florian, Engelbert, Pankratius, Ruprecht, Konstantin, Eusebius, Sebaldus, Cornelius, Archibald, Waldemar
|
||||
names.ogre = Grunz, Klotz, Bums, Brocken, Rumms, Dumpf, Wumme, Klopper, Trampel, Holzkopf, Plumps, Mops, Bolle, Wanst, Knüppel, Ochs, Rumpel, Batzen, Hauruck, Gorbo
|
||||
names.jailer = Grimm, Riegel, Kettler, Schloss, Kerker, Eisenbart, Fessel, Bolzen, Streng, Gitter, Mürrisch, Haken, Strick, Schlüssel, Finster, Klirr, Stockmann, Wächter, Zwinger, Bitter
|
||||
names.priestess = Glut, Flamma, Asche, Seraphine, Lodera, Feuria, Morwen, Brunhild, Ignia, Kriemhild, Scharlach, Funkia, Rabea, Lilith, Zunder, Isolde, Brennhild, Nyx, Vesper, Glimma
|
||||
names.chainbreaker = Kettenbann, Ulfgar, Eisenseele, Brannok, Kragmor, Valdrek, Dorn, Gorath, Ungebunden, Rurik, Torvald, Hagen, Durnok, Sprengel, Brand, Wulfrich, Korrun, Arvak, Sigbert, Halvard
|
||||
names.bat = Piepsi, Flatter, Fiepe, Schwirr, Echo, Flapp, Dämmer, Zischi, Zirp, Zähnchen, Wirbel, Flederli, Motti, Piep, Sausi, Baumel, Samtchen, Huschi, Quiek, Blitzi
|
||||
names.gargoyle = Granit, Schiefer, Basalt, Grimmbert, Mörtel, Kiesel, Schotter, Feuerstein, Fuge, Geröll, Quarz, Bimsstein, Sims, Knorz, Steinbart, Grantig, Giebel, Wasserspeier, Meißel, Sockel
|
||||
names.owl = Uhuwald, Weisling, Kiel, Bartholdo, Minerva, Tobias, Huhu, Eulalia, Federich, Hauch, Grübel, Älteste, Kauzi, Noctua, Eulbert, Pergament, Dämmer, Waldkauz, Professor, Plusch
|
||||
names.wyrm = Vermithrax, Aschenmaul, Sengbart, Calderon, Ignivar, Glimmhorn, Drakkar, Pyrrhus, Obsidian, Glutmaul, Vulkanor, Brennzahn, Malgrath, Tyrax, Inferno, Schwefel, Kaldor, Fafnir, Ragnor, Zinderos
|
||||
names.cogling = Rädchen, Zahnrad, Schräubchen, Ratsche, Bolzen, Getriebe, Tüftel, Dingsbums, Schlüssel, Ritzel, Klimper, Fummel, Niete, Surr, Kurbel, Drehwurm, Flansch, Klackerle, Ticktack, Nippel
|
||||
names.revenant = Morgen, Gram, Aschgrau, Klage, Hohl, Trauer, Bleich, Kummer, Schemen, Moder, Grabmann, Dämmer, Gruft, Wehmut, Wache, Hügel, Leichentuch, Elegie, Neige, Öde
|
||||
names.beast = Zottel, Brummbär, Bruno, Struppi, Mampf, Moosrücken, Brummel, Ursa, Knorr, Knacks, Klicker, Krabbelbert, Borste, Moos, Rumpel, Dornbusch, Zirpel, Grummler, Wuchtel, Schnuffel
|
||||
names.human = Gerhard, Roland, Konrad, Albrecht, Tristan, Eckhart, Gawan, Lienhard, Parzival, Hugo, Bertram, Gottfried, Ortwin, Balduin, Reinhold, Wilfried, Iwein, Gilbert, Ruprecht, Dietrich
|
||||
epithets.mid = Knochenbrecher, Schlammfuß, Rattenbeißer, Dreckgesicht, Kieselkauer, Schädelklopfer, Stinkefaust, Rostknöchel, Bauchboxer, Schienbeintreter, Schimmelbart, Dreckkralle, Rotzschleuder, Ohrenkauer, Kniebrecher, Wurmfresser
|
||||
epithets.high = Königsschreck, Unheilsfaust, Schreckenszahn, Weltenbrecher, Heldenbann, Sturmfaust, Nachtgrauen, Seelenschnitter, Eisenherz, Burgenbrecher, Drachenbann, Höllenwanderer, Schattenfürst, Blutkrone, Grimmflut, Sternbrecher
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Η πύλη καταρρέει! Ο Φύλακας της Πύλ
|
||||
msg.gate_defenders = Η πύλη καλεί {0} υπερασπιστές!
|
||||
msg.level_up = {0}: φτάνει στο επίπεδο {1}.
|
||||
msg.hero_slain = Νικήθηκε: {0}! Λάφυρα: {1} χρυσός.
|
||||
msg.thief_in_treasury = Ένας κλέφτης είναι στο θησαυροφυλάκιό σου!
|
||||
msg.thief_escaped = Ένας κλέφτης ξέφυγε με {0} χρυσό!
|
||||
msg.snare_caught = Πάτησε σε παγίδα: {0}!
|
||||
msg.converted = Ο ιεροεξεταστής προσηλύτισε ένα από τα πλάσματά σου: {0}!
|
||||
msg.creature_died = Έπεσε: {0}.
|
||||
msg.portal_claimed = Η πύλη μετάβασης είναι δική σου! Προσελκύει πλάσματα.
|
||||
msg.flag_removed = Η σημαία επίθεσης αφαιρέθηκε. Τα πλάσματά σου επιστρέφουν.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Δεν φτάνει το μάνα (χρειάζονται {0}
|
||||
hint.portal_no_lair = Η πύλη μετάβασης είναι ανοιχτή, αλλά χωρίς φωλιά δεν θα έρθει κανένα πλάσμα.
|
||||
hint.no_farm = Τα πλάσματα θέλουν να φάνε: χτίσε μανιταρόκηπο.
|
||||
hint.lair_full = Όλα τα κρεβάτια στη φωλιά είναι πιασμένα. Μεγάλωσε τη φωλιά σου.
|
||||
hint.creature_max = Το μπουντρούμι σου έχει 100 πλάσματα - δεν έρχονται άλλα από την πύλη.
|
||||
hint.gate_attack = Τα πλάσματά σου επιτίθενται στην Πύλη Ηρώων!
|
||||
hint.treasury_full = Τα θησαυροφυλάκιά σου γέμισαν! Χτίσε περισσότερα πλακίδια θησαυροφυλακίου.
|
||||
hint.flag_unreachable = Τα πλάσματά σου δεν μπορούν να φτάσουν τη σημαία επίθεσης.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Δεξί κλικ Άφησε μονάδα (αιχμαλώτ
|
||||
help.k_slap = Shift+Αριστερό Χέρι: χαστούκι στις δικές σου μονάδες, γροθιά σε ήρωες στο έδαφός σου
|
||||
help.k_pan = Δεξί σύρσιμο Μετακίνηση χάρτη (και WASD/βέλη, άκρη οθόνης)
|
||||
help.k_rot = Μεσαίο σύρσιμο Περιστροφή και κλίση κάμερας (και Q/E, R/F)
|
||||
help.k_reset = Μεσαίο 2x κλικ Επαναφορά γωνίας και περιστροφής
|
||||
help.k_zoom = Ροδέλα Ζουμ
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Επιλογή εργαλείου
|
||||
help.k_misc = P / Space Παύση M Ήχος ναι/όχι +/- Ένταση
|
||||
@@ -258,6 +264,8 @@ val.medium = Μεσαία
|
||||
val.high = Υψηλή
|
||||
val.normal = Κανονικό
|
||||
val.large = Μεγάλο
|
||||
val.small = Μικρό
|
||||
val.auto_plain = Αυτόματο
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Κάμερα μπροστά
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Ό,τι πέφτει μέσα καίγεται. Οι πέτρι
|
||||
tile.bridge = Ξύλινη γέφυρα
|
||||
tile.lavabridge = Πέτρινη γέφυρα
|
||||
tile.mushroom = Γιγάντιο μανιτάρι
|
||||
tile.mushroom_grown = Ώριμο: έτοιμο για κόψιμο.
|
||||
tile.mushroom_growing = Μεγαλώνει: {0}% - κόβεται μόνο όταν ωριμάσει.
|
||||
tile.grove = Άλσος μανιταριών
|
||||
tile.grove_info = Κανείς δεν μπορεί να κατακτήσει αυτά τα βρύα. Εδώ ξαναφυτρώνουν γιγάντια μανιτάρια.
|
||||
tile.tree = Δέντρο
|
||||
tile.plant_info = Σημάδεψέ το με την αξίνα: ένα διαβολάκι το κόβει για {0} ξύλο.
|
||||
tile.drawbridge = Κινητή γέφυρα
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Ανοιχτές αναμετρήσεις
|
||||
net.no_rooms = Καμία ανοιχτή αναμέτρηση.
|
||||
net.create = Νέα αναμέτρηση
|
||||
net.map_size = Χάρτης {0}x{1} · {2} παίκτες
|
||||
net.coop_mission = Συνεργατική εκστρατεία: αποστολή {0} ({1})
|
||||
net.coop_create = Συνεργατική εκστρ.
|
||||
net.coop_editor = Εκστρατεία επεξεργαστή
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = γιατρεύεται κατά το ένα τρίτο τη
|
||||
unit.giant = Γίγαντας
|
||||
unit.fairy = Νεράιδα Κατάσκοπος
|
||||
unit.battlemage = Μάγος Μάχης
|
||||
unit.thief = Κλέφτης
|
||||
unit.monk = Μοναχός
|
||||
unit.ranger = Δασοφύλακας
|
||||
unit.inquisitor = Ιεροεξεταστής
|
||||
unit.ram = Πολιορκητικός κριός
|
||||
unit.catapult = Καταπέλτης
|
||||
msg.trap_wrecked = Οι ήρωες κατέστρεψαν μια παγίδα: {0}.
|
||||
best.giant.text = Γίγαντας είναι ό,τι γίνεται όταν ένας ιππότης τρώει τα λαχανικά του για τριακόσια χρόνια. Αργός, τεράστιος, και θεωρεί τις πόρτες απλή πρόταση. | Καταστρέφει παγίδες απλώς στεκόμενος δίπλα τους. Αποκρουστικοί τρόποι. Χτύπα τον με ό,τι έχεις, κατά προτίμηση από μακριά.
|
||||
best.fairy.text = Ο μικρός καταδότης του Τάγματος: μια φωτεινή σκνίπα με φτερά, που στέλνεται μπροστά να βρει τις παγίδες σου και να τα πει σε όλους. Μόλις δει μια παγίδα, οι ήρωες τη σπάνε. | Ραντίζει επίσης τους ήρωες με θεραπευτική σκόνη. Λιώσ' την πρώτη. Δύο φορές, για σιγουριά.
|
||||
best.battlemage.text = Ένας μάγος με τον χιτώνα του Τάγματος που θεωρεί τους τοίχους προσωπική προσβολή. Οι ενισχυμένοι τοίχοι σκάνε κάτω από τις πύρινες σφαίρες του, και δεν σταματά ποτέ να το λέει. | Εύθραυστος από κοντά. Οι ατσάλινοι τοίχοι τον εκνευρίζουν περισσότερο - ωραία.
|
||||
best.thief.text = Γρήγορα δάχτυλα, ακόμα πιο γρήγορα πόδια. Δεν έρχεται για να πολεμήσει: γλιστράει στο θησαυροφυλάκιό σου, γεμίζει το σακί του και τρέχει πίσω στην πύλη. | Σκότωσέ τον πριν ξεφύγει και ο χρυσός είναι ξανά δικός σου - με τόκο.
|
||||
best.monk.text = Χρόνια νηστείας και προσευχής τον έκαναν κουφό στη μαγεία σου: ο κεραυνός, η πανούκλα και ο παγετός γλιστρούν πάνω του σαν βροχή σε στέγη. | Και οι γροθιές πονάνε. Υποδέξου τον με ατσάλι, νύχια και δόντια.
|
||||
best.ranger.text = Ένας άνθρωπος του δάσους από το Τάγμα που ρίχνει από μακριά και αφήνει σιδερένιες παγίδες στους διαδρόμους σου. Όποιο πλάσμα πατήσει σε μία τραυματίζεται και κολλάει για λίγο. | Οι παγίδες μένουν ώσπου να πατήσει κάποιος μέσα. Καλύτερα ένας κόμπολντ παρά ο όγκρος σου.
|
||||
best.inquisitor.text = Ασθενικός, αλλά τα κηρύγματά του είναι επικίνδυνα: κάθε είκοσι δευτερόλεπτα πείθει ένα από τα πιο αδύναμα πλάσματά σου - έναν κόμπολντ ή μια αράχνη - να περάσει στους ήρωες. | Τα δυνατά πλάσματα απλώς τον κοροϊδεύουν. Χτύπα πρώτος.
|
||||
best.ram.text = Ένας κορμός με σιδερένια κεφαλή και στέγη για τα βέλη. Τα πλάσματά σου δεν τον νοιάζουν – οι πόρτες, οι τοίχοι και οι πύργοι σου ναι. | Σταμάτα τον πριν φτάσει στην πόρτα, ή βρες καινούργια.
|
||||
best.catapult.text = Ένας ξύλινος βραχίονας, ένας κουβάς πέτρες και ένας ιππότης με πολύ ελεύθερο χρόνο. Βάλλει πιο μακριά από τους πύργους σου και τους σημαδεύει πρώτους. | Όρμα του: από κοντά δεν πειράζει ούτε μύγα.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Φλέβα ρουμπινιού
|
||||
ed.terrain.diamond = Φλέβα διαμαντιού
|
||||
ed.terrain.steel = Ατσάλινος τοίχος
|
||||
ed.terrain.mushroom = Γιγάντιο μανιτάρι
|
||||
ed.terrain.grove = Άλσος μανιταριών
|
||||
ed.terrain.grass = Γρασίδι (επιφάνεια)
|
||||
ed.terrain.tree = Δέντρο
|
||||
ed.place.core = Πυρήνας ψυχής
|
||||
@@ -1354,6 +1376,8 @@ blk.reveal = Αποκάλυψη
|
||||
blk.camera = Δείξε
|
||||
blk.gold = Χρυσός +
|
||||
blk.mana = Μάνα +
|
||||
blk.weather = Καιρός στην επιφάνεια
|
||||
blk.talentxp = Εμπειρία ταλέντων +
|
||||
blk.if = Αν
|
||||
blk.repeat = Επανάληψη
|
||||
blk.trigger = Διακόπτης εναύσματος
|
||||
@@ -1373,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 = Ενεργοποιεί ένα έναυσμα (μπορεί να ξαναπυροδοτηθεί) ή το απενεργοποιεί.
|
||||
@@ -1395,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 = σε
|
||||
@@ -1443,6 +1470,7 @@ blk.until = Επανάληψη ώσπου
|
||||
blk.dountil = Επανάληψη, μετά έλεγχος: ώσπου
|
||||
blk.wait = Αναμονή
|
||||
blk.w_seconds = δευτερόλεπτα
|
||||
blk.w_for = για
|
||||
blk.help.until = Εκτελεί τα μπλοκ μέσα ξανά και ξανά ώσπου να ισχύσει η συνθήκη - ελέγχεται πριν από κάθε εκτέλεση, οπότε αν ισχύει ήδη, το εσωτερικό δεν εκτελείται καθόλου.
|
||||
blk.help.dountil = Εκτελεί τα μπλοκ μέσα, μετά ελέγχει τη συνθήκη και επαναλαμβάνει ώσπου να ισχύσει - το εσωτερικό εκτελείται πάντα τουλάχιστον μία φορά.
|
||||
blk.help.wait = Σταματά αυτό το σενάριο για μερικά δευτερόλεπτα, και μετά συνεχίζει με το επόμενο μπλοκ (και μέσα σε βρόχους).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Στραβοδάχτυλος: Χα! Το άκουσες; Κάτι έ
|
||||
tut.obj6 = Νίκησε τους δύο ιππότες.
|
||||
tut.knights = Στραβοδάχτυλος: Δύο ιππότες! Στείλε τα πλάσματά σου με τη σημαία επίθεσης (Εντολές) ή χτύπησέ τους με κεραυνό.
|
||||
tut.win = Στραβοδάχτυλος: Μπράβο, αφέντη! Αυτό ήταν όλο. Ε - σχεδόν. Τα υπόλοιπα θα τα πει η εκστρατεία. Βάζω νερό για τσάι.
|
||||
|
||||
# master talents
|
||||
cat.master = Άρχοντας
|
||||
talent.title = Ταλέντα του άρχοντα
|
||||
talent.points = Ελεύθεροι πόντοι ταλέντου: {0}
|
||||
talent.next = {0}/{1} εμπειρία ως τον επόμενο πόντο
|
||||
talent.all = Όλα τα ταλέντα έχουν μαθευτεί.
|
||||
talent.builder = Χτίστης
|
||||
talent.warlord = Πολέμαρχος
|
||||
talent.warlock = Μαυρομάγος
|
||||
talent.merchant = Έμπορος
|
||||
talent.builder.desc = Τα δαιμόνια σκάβουν {0}% πιο γρήγορα, τα δωμάτια κοστίζουν {1}% λιγότερο.
|
||||
talent.warlord.desc = Τα πλάσματα χτυπούν {0}% πιο δυνατά και δέχονται {1}% λιγότερη ζημιά.
|
||||
talent.warlock.desc = Η μάνα ρέει {0}% πιο γρήγορα, τα ξόρκια κοστίζουν {1}% λιγότερο.
|
||||
talent.merchant.desc = Οι μισθοί είναι {0}% χαμηλότεροι, ο παραδομένος θησαυρός αξίζει {1}% περισσότερο.
|
||||
talent.none = Δεν έχει μαθευτεί ακόμη.
|
||||
talent.then = Επόμενο επίπεδο: {0}
|
||||
msg.talent_point = Νέος πόντος ταλέντου! Ξόδεψέ τον στο παράθυρο του άρχοντα.
|
||||
msg.talent_learned = {0}: μαθεύτηκε το επίπεδο {1}.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Αίθριο
|
||||
weather.rain = Βροχή
|
||||
weather.fog = Ομίχλη
|
||||
weather.storm = Καταιγίδα
|
||||
msg.weather_clear = Στην επιφάνεια ο ουρανός καθαρίζει.
|
||||
msg.weather_rain = Στην επιφάνεια αρχίζει να βρέχει - η φωτιά δεν πιάνει.
|
||||
msg.weather_fog = Η ομίχλη απλώνεται στη γη - έξω όλοι βλέπουν λιγότερο.
|
||||
msg.weather_storm = Στην επιφάνεια πλησιάζει καταιγίδα! Οι κεραυνοί χτυπούν μονάδες και δέντρα.
|
||||
msg.lightning_unit = Χτυπήθηκε από κεραυνό: {0}!
|
||||
# creature names
|
||||
names.imp = Ψίχουλος, Μύξας, Σκαφτούλης, Λασπάκιας, Χνούδης, Μπόμπιρας, Τρυπούλης, Βρομίτσας, Τσίμπλης, Γρατζούνης, Σβόλος, Λεκές, Βουρκάκης, Κουτσουλιάς, Κοτρωνάκης, Φτυαράκης, Μουτσούνης, Τσουτσέκος, Πιτσιλάκης, Σκουληκάς
|
||||
names.kobold = Γαβγίσης, Τσιμπούκος, Δοντάς, Ψωριάρης, Δαγκάνας, Σκουριάς, Σβουρίτσας, Μασουλάς, Χάχας, Αρουραίος, Κουρέλης, Αγκάθης, Νυχάς, Καβγατζής, Ρίζος, Χαχανούλης, Σπρωξάκης, Φαφούτης, Κρακ, Γρατζουνάς
|
||||
names.spider = Ποδαράκια, Μαλλιαρούλα, Αραχνούλα, Γαργαλίτσα, Διχτάκι, Οχτάποδη, Κλωστίτσα, Τρομαράκι, Ψαχουλίτσα, Μεταξούλα, Υφάντρα, Χοπ, Χνουδούλα, Θέκλα, Καρούλι, Αρπαχτούλα, Κουβαράκι, Κολλιτσίδα, Σβούρα, Βελονίτσα
|
||||
names.mage = Βαρθολομαίος, Ιγνάτιος, Λεοπόλδος, Θεοβάλδος, Πολυχρόνιος, Ιερώνυμος, Χαραλάμπης, Παντελεήμων, Κορνήλιος, Θεμιστοκλής, Φλωρέντιος, Ευστάθιος, Παγκράτιος, Αρχέλαος, Κωνσταντίνος, Ευσέβιος, Σεβηρίνος, Βαρνάβας, Τιμολέων, Αναστάσιος
|
||||
names.ogre = Μπουμπούνας, Κούτσουρο, Μπαμ, Βράχος, Χαζούλης, Ντουβάρι, Χοντρούλης, Κοπανιστής, Ποδοβολάς, Ξυλοκέφαλος, Πλατς, Κοιλαράς, Μπουλούκος, Τουμπανάς, Ρόπαλο, Βόδι, Γδούπος, Ογκόλιθος, Τύμπανο, Γκόρμπο
|
||||
names.jailer = Σκυθρωπός, Σύρτης, Αλυσίδας, Λουκέτος, Μπουντρούμης, Σιδερογένης, Κρίκος, Μπουλόνι, Αυστηρός, Κάγκελος, Γκρινιάρης, Γάντζος, Θηλιάς, Κλειδούχος, Ζοφερός, Κουδουνάς, Φρουράρχος, Δεσμοφύλακας, Κλουβής, Πικρός
|
||||
names.priestess = Θράκα, Φλόγα, Στάχτη, Σεραφίνα, Πυρά, Φωτιά, Μόργκανα, Βρουνχίλδη, Ιγνία, Σπίθα, Πορφύρα, Ανθρακιά, Κοράκω, Λίλιθ, Ίσκα, Ιζόλδη, Λαμπαδίτσα, Νυξ, Εσπερία, Φλογίτσα
|
||||
names.chainbreaker = Αλυσοσπάστης, Ούλφγκαρ, Σιδερόψυχος, Μπράννοκ, Κράγκμορ, Βάλντρεκ, Αγκάθι, Γκόραθ, Αδούλωτος, Ρούρικ, Τόρβαλντ, Χάγκεν, Ντούρνοκ, Θραύσμα, Δαυλός, Λυκόψυχος, Κόρρουν, Άρβακ, Ελευθερωτής, Χάλβαρντ
|
||||
names.bat = Τσιρίδας, Φτερουγάκης, Πιπ, Θρόισμα, Ηχώ, Φτερουγίτσα, Σούρουπο, Σφυριχτής, Τιτίβισμα, Δοντάκι, Στρόβιλος, Νυχτεριδάκι, Σκόρος, Πιπίνος, Σβουράκης, Κρεμαστός, Βελούδο, Γλιστράκης, Στρίγκλης, Αστραπή
|
||||
names.gargoyle = Γρανίτης, Σχιστόλιθος, Βασάλτης, Πετρόμουτρος, Κονίαμα, Βότσαλος, Χαλίκης, Πυρόλιθος, Αρμός, Ερειπιάς, Χαλαζίας, Κίσσηρη, Γείσος, Ρόζος, Πετρογένης, Κατσούφης, Αέτωμα, Υδρορρόη, Σμιλάκης, Βάθρος
|
||||
names.owl = Μπούφος, Σοφούλης, Πέννα, Βαρθόλδος, Αθηνά, Τωβίας, Χουχού, Κουκουβάγια, Φτερούλης, Αναστεναγμός, Στοχαστής, Γεροντάκος, Χαμοκελάδα, Γλαύκα, Γλαυκούλης, Περγαμηνή, Λυκόφως, Νυχτοπούλι, Καθηγητής, Πούπουλο
|
||||
names.wyrm = Βερμίθραξ, Σταχτόστομος, Καψαλιάρης, Καλντέρων, Ιγνίβαρ, Πυρόκερος, Δράκκαρ, Πύρρος, Οψιδιανός, Πυρόστομος, Ηφαιστάρας, Καψόδοντος, Μάλγκραθ, Τύραξ, Κόλαση, Θειάφης, Κάλντορ, Φάφνιρ, Ράγκνορ, Δαυλίτης
|
||||
names.cogling = Γραναζάκης, Ροδίτσα, Βιδούλης, Καστάνιας, Πριτσίνι, Μεταδότης, Μαστοράκος, Μαραφέτι, Κλειδάκι, Πινιόν, Κουδουνίτσα, Ψαχούλης, Παξιμάδι, Βουιτζής, Μανιβέλα, Σβούρος, Φλάντζα, Κλικ, Τικτάκ, Ελατηριάκι
|
||||
names.revenant = Θρήνος, Πίκρα, Σταχτής, Βογκητής, Κενός, Πένθος, Χλωμός, Καημός, Σκιά, Χώμα, Νεκροθάφτης, Σούρουπος, Κρύπτη, Νοσταλγός, Άγρυπνος, Τύμβος, Σάβανο, Ελεγεία, Κατακάθι, Ερημιά
|
||||
names.beast = Μαλλιαράς, Αρκουδάκος, Μπρούνο, Τριχωτός, Μασουλίνος, Βρυόραχος, Γρυλιστής, Αρκουδίτσα, Θάμνος, Κριτσανίας, Τσακ, Σέρνας, Σκαθαράς, Βρύο, Βρόντος, Βατομουριάς, Τριζόνης, Γκρινιάρος, Μπάρμπας, Μυριστής
|
||||
names.human = Γεράρδος, Ορλάνδος, Κορράδος, Αλβέρτος, Τριστάνος, Έκαρτ, Γαβαίνος, Λάνσελοτ, Πάρσιφαλ, Ούγκος, Βέρτραμ, Γοδεφρείδος, Όρτβιν, Βαλδουίνος, Ρεϊνάλδος, Γουλιέλμος, Θεοδέριχος, Γκίλμπερτ, Ροβέρτος, Βαλεριανός
|
||||
epithets.mid = Κοκαλοσπάστης, Λασποπόδαρος, Ποντικοφάγος, Βρομόμουτρος, Χαλικομάσης, Κρανιοκόπος, Βρομογροθιάς, Σκουριόκοκαλος, Κοιλιοσπάστης, Καλαμοκλότσης, Μουχλογένης, Βρομόνυχος, Μυξοπετάχτης, Αυτιοφάγος, Γονατοσπάστης, Σκουληκοφάγος
|
||||
epithets.high = Βασιλοκτόνος, Ολεθροχέρης, Τρομόδοντος, Κοσμοσπάστης, Ηρωοφόνος, Θυελλογροθιάς, Νυχτότρομος, Ψυχοθερίστης, Σιδερόκαρδος, Καστροσπάστης, Δρακοκτόνος, Αδοδιαβάτης, Σκιάρχης, Αιματοστέφανος, Ολεθρόπλημμυρος, Αστροσπάστης
|
||||
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 = θάνατος στη μάχη
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = The gate is crumbling! The Gate Warden appears with {0} defender
|
||||
msg.gate_defenders = The gate summons {0} defenders!
|
||||
msg.level_up = {0} reaches level {1}.
|
||||
msg.hero_slain = {0} defeated! Loot: {1} gold.
|
||||
msg.thief_in_treasury = A thief is in your treasury!
|
||||
msg.thief_escaped = A thief escaped with {0} gold!
|
||||
msg.snare_caught = Your {0} stepped into a snare!
|
||||
msg.converted = The inquisitor turned your {0} against you!
|
||||
msg.creature_died = Your {0} has fallen.
|
||||
msg.portal_claimed = The portal is yours! It attracts creatures.
|
||||
msg.flag_removed = Attack flag removed. Your creatures return.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Not enough mana ({0} needed).
|
||||
hint.portal_no_lair = The portal is open, but without a lair no creatures will come.
|
||||
hint.no_farm = Creatures want to eat: build a mushroom garden.
|
||||
hint.lair_full = All beds in the lair are taken. Enlarge your lair.
|
||||
hint.creature_max = Your dungeon holds 100 creatures - no more come through the portal.
|
||||
hint.gate_attack = Your creatures are attacking the Hero Gate!
|
||||
hint.treasury_full = Your treasuries are full! Build more treasury tiles.
|
||||
hint.flag_unreachable = Your creatures cannot reach the attack flag.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Right click Drop a unit (prisoners: also into the torture cha
|
||||
help.k_slap = Shift+Left Hand: backhand own units, punch heroes on your ground
|
||||
help.k_pan = Right drag Move the map (also WASD/arrows, screen edge)
|
||||
help.k_rot = Middle drag Rotate and tilt the camera (also Q/E, R/F)
|
||||
help.k_reset = Middle 2x click Reset angle and turn (the place stays)
|
||||
help.k_zoom = Mouse wheel Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Select tool
|
||||
help.k_misc = P / Space Pause M Sound on/off +/- Volume
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Whatever falls in burns. Stone bridges cross it.
|
||||
tile.bridge = Wooden bridge
|
||||
tile.lavabridge = Stone bridge
|
||||
tile.mushroom = Giant mushroom
|
||||
tile.mushroom_grown = Fully grown: ready for felling.
|
||||
tile.mushroom_growing = Growing: {0}% - it can only be felled when fully grown.
|
||||
tile.grove = Mushroom grove
|
||||
tile.grove_info = Nobody can claim this moss. Giant mushrooms grow back here.
|
||||
tile.tree = Tree
|
||||
tile.plant_info = Mark it with the pick: an imp fells it for {0} wood.
|
||||
tile.drawbridge = Drawbridge
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Open matches
|
||||
net.no_rooms = No open matches.
|
||||
net.create = Create match
|
||||
net.map_size = Map {0}x{1} · {2} players
|
||||
net.coop_mission = Co-op campaign: mission {0} ({1})
|
||||
net.coop_create = Co-op campaign
|
||||
net.coop_editor = Editor campaign
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = heals itself by a third of the damage it deals.
|
||||
unit.giant = Giant
|
||||
unit.fairy = Fairy Spy
|
||||
unit.battlemage = Battle Mage
|
||||
unit.thief = Thief
|
||||
unit.monk = Monk
|
||||
unit.ranger = Ranger
|
||||
unit.inquisitor = Inquisitor
|
||||
unit.ram = Battering ram
|
||||
unit.catapult = Catapult
|
||||
msg.trap_wrecked = The heroes wrecked a trap: {0}.
|
||||
best.giant.text = A giant is what happens when a knight eats his vegetables for three hundred years. Slow, enormous, and it treats doors as a suggestion. | It wrecks traps simply by standing next to them. Revolting manners. Hit it with everything, preferably from afar.
|
||||
best.fairy.text = The Order's little informer: a glowing gnat with wings, sent ahead to find your traps and tell everybody. Once it has seen a trap, the heroes smash it. | It also sprinkles healing dust on the heroes. Swat it first. Twice, to be sure.
|
||||
best.battlemage.text = A mage in the Order's robes who thinks walls are a personal insult. Reinforced walls burst under his fireballs, and he never stops talking about it. | Fragile up close. Steel walls annoy him most - good.
|
||||
best.thief.text = Quick fingers, quicker feet. He does not come to fight: he sneaks into your treasury, fills his sack and runs back to the gate. | Kill him before he gets away and the gold is yours again - with interest.
|
||||
best.monk.text = Years of fasting and praying have made him deaf to your magic: lightning, plague and frost slide off him like rain off a roof. | Fists hurt too. Meet him with steel, claws and teeth.
|
||||
best.ranger.text = A woodsman of the Order who shoots from afar and leaves iron snares in your corridors. A creature that steps into one is hurt and stuck for a while. | Snares stay until something steps in. Better a kobold than your ogre.
|
||||
best.inquisitor.text = Frail, but his sermons are dangerous: every twenty seconds he talks one of your weakest creatures - a kobold or a spider - into joining the heroes. | Strong creatures just laugh at him. Hit him first.
|
||||
best.ram.text = A tree trunk with an iron cap and a roof against arrows. It does not care for your creatures – only for your doors, walls and towers. | Stop it before it reaches the door, or get a new door.
|
||||
best.catapult.text = A wooden arm, a bucket of rocks and a knight with too much spare time. It outranges your towers and aims for them first. | Rush it: up close it cannot hurt a fly.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Ruby vein
|
||||
ed.terrain.diamond = Diamond vein
|
||||
ed.terrain.steel = Steel wall
|
||||
ed.terrain.mushroom = Giant mushroom
|
||||
ed.terrain.grove = Mushroom grove
|
||||
ed.terrain.grass = Grass (above ground)
|
||||
ed.terrain.tree = Tree
|
||||
ed.place.core = Soul core
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Crookfinger: Ha! Did you hear that? Something is coming from the east
|
||||
tut.obj6 = Defeat the two knights.
|
||||
tut.knights = Crookfinger: Two knights! Send your creatures with the attack flag (Orders) or strike them with lightning.
|
||||
tut.win = Crookfinger: Well done, master! That is all there is to it. Well - almost. The campaign tells the rest. I'll put the kettle on.
|
||||
|
||||
# master talents
|
||||
cat.master = Master
|
||||
talent.title = Master talents
|
||||
talent.points = Free talent points: {0}
|
||||
talent.next = {0}/{1} experience to the next point
|
||||
talent.all = Every talent is learned.
|
||||
talent.builder = Builder
|
||||
talent.warlord = Warlord
|
||||
talent.warlock = Warlock
|
||||
talent.merchant = Merchant
|
||||
talent.builder.desc = Imps dig {0}% faster, rooms cost {1}% less.
|
||||
talent.warlord.desc = Creatures hit {0}% harder and take {1}% less damage.
|
||||
talent.warlock.desc = Mana flows {0}% faster, spells cost {1}% less.
|
||||
talent.merchant.desc = Wages are {0}% lower, delivered treasure is worth {1}% more.
|
||||
talent.none = Not learned yet.
|
||||
talent.then = Next level: {0}
|
||||
msg.talent_point = A new talent point! Spend it in the Master window.
|
||||
msg.talent_learned = {0} level {1} learned.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Fair
|
||||
weather.rain = Rain
|
||||
weather.fog = Fog
|
||||
weather.storm = Storm
|
||||
msg.weather_clear = The sky clears up above ground.
|
||||
msg.weather_rain = It starts to rain above ground - fire will not catch.
|
||||
msg.weather_fog = Fog rolls over the land - everyone outside sees less.
|
||||
msg.weather_storm = A storm is brewing above ground! Lightning strikes units and trees.
|
||||
msg.lightning_unit = Lightning struck: {0}!
|
||||
# creature names
|
||||
names.imp = Grubnik, Snot, Pickles, Scrabble, Dimple, Muck, Nibbles, Grot, Twitch, Smudge, Pebble, Wort, Scuttle, Fizzle, Gristle, Sniv, Burrow, Crumb, Dribble, Tinker
|
||||
names.kobold = Yip, Skrag, Ratchet, Snagtooth, Kibble, Rusk, Nettle, Skiv, Gnash, Scrapper, Flint, Yark, Biter, Sprocket, Dreg, Skulk, Tatter, Grizz, Nab, Scabby
|
||||
names.spider = Webbly, Itsy, Nibsy, Silkie, Creepkin, Tickle, Venna, Lacey, Spindle, Skitter, Fangsy, Mopsy, Widow, Tangles, Weaver, Pip, Gossamer, Twiddle, Snare, Muffet
|
||||
names.mage = Bartholomew, Ignatius, Percival, Mordecai, Fennimore, Cornelius, Aldous, Thaddeus, Barnabus, Erasmus, Montague, Ambrose, Septimus, Horatio, Quentin, Lucan, Archibald, Florian, Zebulon, Crispin
|
||||
names.ogre = Grunk, Thud, Bonk, Lump, Mudge, Klod, Hulk, Gorb, Dunk, Brog, Oaf, Plonk, Boulder, Grog, Mungo, Slab, Wump, Tonk, Duh, Gullet
|
||||
names.jailer = Grimm, Lockjaw, Chains, Dredd, Morrow, Shackle, Vex, Bolt, Garrote, Stern, Rook, Hask, Iron, Grist, Clank, Mordo, Bleak, Snare, Kessel, Warden
|
||||
names.priestess = Ember, Seraphine, Pyra, Ashlyn, Scarlet, Morwen, Vesper, Cinder, Ravenna, Lilith, Blaze, Isolde, Brimma, Nyx, Sable, Velvet, Flare, Ophelia, Hestia, Rowan
|
||||
names.chainbreaker = Fetterbane, Ulfgar, Ironsoul, Brannoc, Kragmor, Valdrek, Thorn, Gorath, Unbound, Rurik, Torvald, Hagan, Durnok, Shatter, Brand, Wulfric, Korrun, Arvak, Stigr, Halvard
|
||||
names.bat = Squeak, Flutter, Pippin, Nibs, Echo, Flap, Dusk, Zippy, Chirp, Fang, Twirl, Batsy, Mothy, Peep, Swoop, Dangle, Velvet, Tiddles, Squee, Blip
|
||||
names.gargoyle = Granite, Slate, Basalt, Grimsby, Mortar, Cobble, Gravel, Flint, Grout, Rubble, Quartz, Pumice, Lintel, Gnarl, Stoneface, Dour, Gable, Spout, Chisel, Ledge
|
||||
names.owl = Hootsworth, Sage, Quill, Barnaby, Minerva, Tobias, Hoots, Ollivant, Feathers, Wisp, Ponder, Elder, Mopsy, Noctua, Owlbert, Scroll, Dusk, Tawny, Professor, Ruffle
|
||||
names.wyrm = Vermithrax, Ashmaw, Scorch, Calderon, Ignivar, Smoulder, Drakkar, Pyrrhus, Obsidian, Embermaw, Vulcanor, Searfang, Malgrath, Tyrax, Inferno, Brimstone, Kaldor, Fyrnak, Ragnor, Cindros
|
||||
names.cogling = Gizmo, Sprocket, Widget, Ratchet, Bolt, Gearwin, Tinker, Doohickey, Spanner, Pinion, Clink, Fiddle, Rivet, Whirr, Cogsworth, Twiddle, Flange, Gadget, Ticktock, Nubbin
|
||||
names.revenant = Morrow, Grieve, Ashen, Lament, Hollow, Mourn, Pallor, Sorrow, Wraithe, Dirge, Barrow, Gloam, Tomb, Weep, Vigil, Cairn, Shroud, Elegy, Dregs, Bleak
|
||||
names.beast = Fuzzbert, Grizzle, Bruno, Shaggs, Chomper, Mossback, Bumble, Ursa, Burl, Crunch, Clicker, Scuttles, Bristle, Moss, Rumble, Bramble, Chitter, Grumbles, Hulkster, Snuffle
|
||||
names.human = Gareth, Roland, Cedric, Aldric, Tristan, Edmund, Gawain, Lionel, Percy, Hugo, Bertram, Godfrey, Osric, Baldwin, Reginald, Wilfred, Ector, Gilbert, Rupert, Duncan
|
||||
epithets.mid = Bonecrusher, Mudfoot, Ratbiter, Grimeface, Pebblechewer, Skullthumper, Stinkfist, Rustknuckle, Gutpuncher, Shinkicker, Mouldbeard, Dirtclaw, Snotslinger, Earchewer, Kneebreaker, Wormeater
|
||||
epithets.high = Kingslayer, Doomhand, Dreadfang, Worldbreaker, Herobane, Stormfist, Nightterror, Soulreaper, Ironheart, Castlecrusher, Dragonbane, Hellstrider, Shadowlord, Bloodcrown, Grimtide, Starbreaker
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = ¡La puerta se desmorona! ¡El Guardián de la Puerta aparece co
|
||||
msg.gate_defenders = ¡La puerta invoca a {0} defensores!
|
||||
msg.level_up = {0} alcanza el nivel {1}.
|
||||
msg.hero_slain = ¡{0} derrotado! Botín: {1} de oro.
|
||||
msg.thief_in_treasury = ¡Hay un ladrón en tu tesorería!
|
||||
msg.thief_escaped = ¡Un ladrón ha escapado con {0} de oro!
|
||||
msg.snare_caught = ¡Ha caído en un cepo: {0}!
|
||||
msg.converted = ¡El inquisidor ha convertido a una de tus criaturas: {0}!
|
||||
msg.creature_died = Tu {0} ha caído.
|
||||
msg.portal_claimed = ¡El portal es tuyo! Atrae a las criaturas.
|
||||
msg.flag_removed = Bandera de ataque retirada. Tus criaturas regresan.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = No hay maná suficiente (se necesitan {0}).
|
||||
hint.portal_no_lair = El portal está abierto, pero sin guarida no vendrá ninguna criatura.
|
||||
hint.no_farm = Las criaturas quieren comer: construye un huerto de setas.
|
||||
hint.lair_full = Todas las camas de la guarida están ocupadas. Amplía tu guarida.
|
||||
hint.creature_max = Tu mazmorra alberga 100 criaturas: no vendrán más por el portal.
|
||||
hint.gate_attack = ¡Tus criaturas atacan la Puerta de los Héroes!
|
||||
hint.treasury_full = ¡Tus tesorerías están llenas! Construye más casillas de tesorería.
|
||||
hint.flag_unreachable = Tus criaturas no pueden llegar a la bandera de ataque.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Clic derecho Soltar unidad (prisioneros: también en la cámar
|
||||
help.k_slap = Mayús+Izq. Mano: abofetear unidades propias, golpear héroes en tu suelo
|
||||
help.k_pan = Arrastrar dcho. Mover el mapa (también WASD/flechas, borde de pantalla)
|
||||
help.k_rot = Arrastrar medio Rotar e inclinar la cámara (también Q/E, R/F)
|
||||
help.k_reset = Medio 2x clic Restablecer ángulo y giro (el lugar se queda)
|
||||
help.k_zoom = Rueda del ratón Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Elegir herramienta
|
||||
help.k_misc = P / Espacio Pausa M Sonido sí/no +/- Volumen
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Lo que cae dentro arde. Los puentes de piedra lo cruzan.
|
||||
tile.bridge = Puente de madera
|
||||
tile.lavabridge = Puente de piedra
|
||||
tile.mushroom = Seta gigante
|
||||
tile.mushroom_grown = Adulto: listo para talar.
|
||||
tile.mushroom_growing = Creciendo: {0}% - solo se puede talar cuando esté adulto.
|
||||
tile.grove = Bosquecillo de setas
|
||||
tile.grove_info = Nadie puede conquistar este musgo. Aquí vuelven a crecer setas gigantes.
|
||||
tile.tree = Árbol
|
||||
tile.plant_info = Márcalo con el pico: un diablillo lo tala por {0} de madera.
|
||||
tile.drawbridge = Puente levadizo
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Partidas abiertas
|
||||
net.no_rooms = No hay partidas abiertas.
|
||||
net.create = Crear partida
|
||||
net.map_size = Mapa {0}x{1} · {2} jugadores
|
||||
net.coop_mission = Campaña cooperativa: misión {0} ({1})
|
||||
net.coop_create = Campaña coop.
|
||||
net.join = Unirse
|
||||
@@ -1075,12 +1088,20 @@ ability.drain.desc = se cura un tercio del daño que inflige.
|
||||
unit.giant = Gigante
|
||||
unit.fairy = Hada espía
|
||||
unit.battlemage = Mago de batalla
|
||||
unit.thief = Ladrón
|
||||
unit.monk = Monje
|
||||
unit.ranger = Explorador
|
||||
unit.inquisitor = Inquisidor
|
||||
unit.ram = Ariete
|
||||
unit.catapult = Catapulta
|
||||
msg.trap_wrecked = Los héroes han destrozado una trampa: {0}.
|
||||
best.giant.text = Un gigante es lo que pasa cuando un caballero se come la verdura durante trescientos años. Lento, enorme, y trata las puertas como una sugerencia. | Destroza trampas con solo plantarse a su lado. Modales repugnantes. Golpéalo con todo, a ser posible desde lejos.
|
||||
best.fairy.text = La pequeña chivata de la Orden: un mosquito brillante con alas, enviado por delante para encontrar tus trampas y contárselo a todos. Cuando ha visto una trampa, los héroes la destrozan. | También rocía polvo curativo sobre los héroes. Aplástala primero. Dos veces, por si acaso.
|
||||
best.battlemage.text = Un mago con el hábito de la Orden que considera los muros un insulto personal. Los muros reforzados revientan bajo sus bolas de fuego, y nunca deja de hablar de ello. | Frágil de cerca. Lo que más le fastidia son los muros de acero. Bien.
|
||||
best.thief.text = Dedos rápidos, pies aún más rápidos. No viene a luchar: se cuela en tu tesorería, llena su saco y corre de vuelta a la puerta. | Mátalo antes de que escape y el oro vuelve a ser tuyo. Con intereses.
|
||||
best.monk.text = Años de ayuno y oración lo han vuelto sordo a tu magia: el rayo, la plaga y el frío le resbalan como la lluvia por un tejado. | Los puños también duelen. Recíbelo con acero, garras y dientes.
|
||||
best.ranger.text = Un hombre de los bosques de la Orden que dispara desde lejos y deja cepos de hierro en tus pasillos. La criatura que pisa uno queda herida y atrapada un rato. | Los cepos siguen ahí hasta que alguien los pisa. Mejor un kóbold que tu ogro.
|
||||
best.inquisitor.text = Frágil, pero sus sermones son peligrosos: cada veinte segundos convence a una de tus criaturas más débiles (un kóbold o una araña) de unirse a los héroes. | Las criaturas fuertes se ríen de él. Golpea tú primero.
|
||||
best.ram.text = Un tronco con punta de hierro y un tejado contra las flechas. Tus criaturas le dan igual; tus puertas, muros y torres no. | Detenlo antes de que llegue a la puerta, o búscate otra puerta.
|
||||
best.catapult.text = Un brazo de madera, un cubo de piedras y un caballero con demasiado tiempo libre. Alcanza más lejos que tus torres y apunta a ellas primero. | Lánzate sobre ella: de cerca no hace daño ni a una mosca.
|
||||
|
||||
@@ -1264,6 +1285,7 @@ ed.terrain.ruby = Veta de rubíes
|
||||
ed.terrain.diamond = Veta de diamantes
|
||||
ed.terrain.steel = Muro de acero
|
||||
ed.terrain.mushroom = Seta gigante
|
||||
ed.terrain.grove = Bosquecillo de setas
|
||||
ed.terrain.grass = Hierba (superficie)
|
||||
ed.terrain.tree = Árbol
|
||||
ed.place.core = Núcleo de alma
|
||||
@@ -1346,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
|
||||
@@ -1365,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.
|
||||
@@ -1387,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Dedotuerto: ¡Ja! ¿Lo has oído? Algo viene del este. Prepárate: el
|
||||
tut.obj6 = Derrota a los dos caballeros.
|
||||
tut.knights = Dedotuerto: ¡Dos caballeros! Envía a tus criaturas con la bandera de ataque (Órdenes) o fulmínalos con el rayo.
|
||||
tut.win = Dedotuerto: ¡Bien hecho, amo! Eso es todo. Bueno, casi. La campaña cuenta el resto. Voy a poner el agua a hervir.
|
||||
|
||||
# master talents
|
||||
cat.master = Maestro
|
||||
talent.title = Talentos del maestro
|
||||
talent.points = Puntos de talento libres: {0}
|
||||
talent.next = {0}/{1} de experiencia hasta el siguiente punto
|
||||
talent.all = Todos los talentos están aprendidos.
|
||||
talent.builder = Constructor
|
||||
talent.warlord = Caudillo
|
||||
talent.warlock = Brujo
|
||||
talent.merchant = Mercader
|
||||
talent.builder.desc = Los diablillos cavan un {0}% más rápido y las salas cuestan un {1}% menos.
|
||||
talent.warlord.desc = Las criaturas golpean un {0}% más fuerte y reciben un {1}% menos de daño.
|
||||
talent.warlock.desc = El maná fluye un {0}% más rápido y los hechizos cuestan un {1}% menos.
|
||||
talent.merchant.desc = Los sueldos son un {0}% más bajos y el tesoro entregado vale un {1}% más.
|
||||
talent.none = Aún no aprendido.
|
||||
talent.then = Siguiente nivel: {0}
|
||||
msg.talent_point = ¡Un nuevo punto de talento! Gástalo en la ventana del maestro.
|
||||
msg.talent_learned = {0}: nivel {1} aprendido.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Despejado
|
||||
weather.rain = Lluvia
|
||||
weather.fog = Niebla
|
||||
weather.storm = Tormenta
|
||||
msg.weather_clear = El cielo se despeja en la superficie.
|
||||
msg.weather_rain = Empieza a llover en la superficie: el fuego no prende.
|
||||
msg.weather_fog = La niebla cubre la tierra: todos ven menos al aire libre.
|
||||
msg.weather_storm = ¡Se avecina una tormenta en la superficie! Los rayos alcanzan unidades y árboles.
|
||||
msg.lightning_unit = ¡Un rayo alcanza a {0}!
|
||||
# creature names
|
||||
names.imp = Mugrín, Mocoso, Cascarrín, Pringue, Garbancito, Roñito, Pelusa, Zurullo, Tizón, Chafardín, Escarbín, Migaja, Chispín, Lodito, Gusarapo, Rascón, Pocholo, Terrón, Pipo, Zarpín
|
||||
names.kobold = Gruñín, Raspa, Mordisco, Chirlo, Trapo, Tarugo, Zarpazo, Rufo, Pellizco, Garfio, Cardo, Chuzo, Zorrino, Pícaro, Ratero, Mellado, Rasguño, Guijarro, Cuchi, Tiznado
|
||||
names.spider = Patitas, Telarín, Hilacha, Cosquillas, Ocho, Sedita, Tejedora, Pelusín, Brujilla, Enredo, Ovillo, Agujita, Trampita, Colmilla, Lunares, Retales, Bordadora, Pinchito, Sombrita, Telita
|
||||
names.mage = Bartolomé, Ignacio, Hermenegildo, Anacleto, Fulgencio, Eustaquio, Leopoldo, Baltasar, Melquíades, Teodoro, Segismundo, Florencio, Casimiro, Gumersindo, Nicanor, Aurelio, Rigoberto, Cipriano, Marcelino, Wenceslao
|
||||
names.ogre = Pedrusco, Ceporro, Zoquete, Mamotreto, Tocho, Bruto, Mazacote, Bolo, Morlaco, Pancho, Trompo, Tronco, Cachalote, Pataca, Mendrugo, Bombo, Gordinflón, Porrazo, Garrulo, Borrico
|
||||
names.jailer = Cerrojo, Candado, Grilletes, Ceñudo, Sombrío, Mazmorro, Rejas, Torvo, Hierro, Cadenas, Severo, Aldaba, Garrote, Cancerbero, Llavero, Lúgubre, Hosco, Calabozo, Tranca, Argolla
|
||||
names.priestess = Brasa, Ascua, Llama, Pira, Centella, Ceniza, Bermeja, Hoguera, Fulgor, Lumbre, Morgana, Serafina, Escarlata, Chispa, Ignea, Rubí, Ámbar, Fénix, Candela, Pavesa
|
||||
names.chainbreaker = Rompecadenas, Ulfgar, Almahierro, Brannok, Kragmor, Valdrek, Espino, Gorath, Indómito, Rurik, Torvaldo, Hagan, Durnok, Quebranto, Tizona, Wulfrido, Korrún, Arvak, Libertador, Halvardo
|
||||
names.bat = Chillido, Aletas, Pío, Mordi, Eco, Batí, Ocaso, Zumbi, Chirri, Colmillito, Revolo, Murci, Polilla, Piquito, Picado, Colgandero, Terciopelo, Nocturno, Chirrín, Zigzag
|
||||
names.gargoyle = Granito, Pizarra, Basalto, Adoquín, Mortero, Guijarro, Grava, Pedernal, Cascote, Escombro, Cuarzo, Pómez, Cornisa, Gárgola, Carapiedra, Sillar, Frontón, Cantero, Cincel, Peñasco
|
||||
names.owl = Sabiondo, Ulular, Pluma, Buhíto, Minerva, Tobías, Lechuzo, Sapiencio, Plumón, Sabio, Cavilón, Anciano, Mochuelo, Noctua, Búho, Pergamino, Ocaso, Cárabo, Profesor, Erudito
|
||||
names.wyrm = Vermitrax, Fauceniza, Chamusco, Calderón, Ignívoro, Ascuamor, Drakkar, Pirro, Obsidiano, Brasamor, Vulcanor, Abrasador, Malgrath, Tyrax, Infierno, Azufre, Kaldor, Fyrnak, Ragnor, Cindros
|
||||
names.cogling = Tuerquita, Engranaje, Tornillo, Trinquete, Perno, Piñón, Chirimbolo, Cachivache, Llavecita, Muelle, Tintín, Chisme, Remache, Zumbido, Manivela, Arandela, Bisagra, Artilugio, Tictac, Resorte
|
||||
names.revenant = Lamento, Pesar, Ceniciento, Quebranto, Hueco, Luto, Lívido, Pena, Espectro, Endecha, Túmulo, Penumbra, Cripta, Llanto, Vigilia, Mortaja, Sudario, Elegía, Ánima, Desolado
|
||||
names.beast = Greñas, Melenudo, Bruno, Pelambre, Zampón, Musgoso, Gruñón, Osezno, Matorral, Crujido, Cascarón, Patudo, Cerdas, Moho, Retumbo, Zarza, Chasquido, Rezongón, Mole, Hocicón
|
||||
names.human = Rodrigo, Gonzalo, Álvaro, Fernando, Tristán, Sancho, Bermudo, Lope, Pelayo, Hugo, Beltrán, Godofredo, Ordoño, Balduino, Ramiro, Gutierre, Nuño, Gilberto, Rufino, Diego
|
||||
epithets.mid = Rompehuesos, Pisabarro, Muerderratas, Caramugre, Mascapiedras, Machacacráneos, Puñoapestoso, Nudillorroñoso, Revientapanzas, Pateaespinillas, Barbamoho, Garramugre, Lanzamocos, Masticaorejas, Parterrodillas, Comegusanos
|
||||
epithets.high = Matarreyes, Manofunesta, Colmillopavor, Rompemundos, Azotehéroes, Puñotormenta, Terrornocturno, Segadalmas, Corazóndehierro, Derribacastillos, Matadragones, Andainfiernos, Señorsombra, Coronasangre, Mareafunesta, Quiebrastros
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Portti horjuu! Portinvartija ilmestyy, mukanaan {0} puolustajaa!
|
||||
msg.gate_defenders = Portti kutsuu {0} puolustajaa!
|
||||
msg.level_up = {0} nousee tasolle {1}.
|
||||
msg.hero_slain = Kaatui: {0}! Saalis: {1} kultaa.
|
||||
msg.thief_in_treasury = Varas on aarreaitassasi!
|
||||
msg.thief_escaped = Varas pakeni {0} kullan kanssa!
|
||||
msg.snare_caught = Astui ansaan: {0}!
|
||||
msg.converted = Inkvisiittori käännytti yhden olennoistasi: {0}!
|
||||
msg.creature_died = Olentosi kaatui: {0}.
|
||||
msg.portal_claimed = Portaali on sinun! Se houkuttelee olentoja.
|
||||
msg.flag_removed = Hyökkäyslippu poistettu. Olentosi palaavat.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Mana ei riitä (tarvitaan {0}).
|
||||
hint.portal_no_lair = Portaali on auki, mutta ilman pesää yksikään olento ei tule.
|
||||
hint.no_farm = Olennot haluavat syödä: rakenna sienitarha.
|
||||
hint.lair_full = Pesän kaikki vuoteet ovat varattuja. Laajenna pesää.
|
||||
hint.creature_max = Luolastossasi asuu 100 olentoa - portaalista ei tule enempää.
|
||||
hint.gate_attack = Olentosi hyökkäävät sankariporttia vastaan!
|
||||
hint.treasury_full = Aarreaittasi ovat täynnä! Rakenna lisää aarreaittaruutuja.
|
||||
hint.flag_unreachable = Olentosi eivät pääse hyökkäyslipun luo.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Oikea klikkaus Pudota yksikkö (vangit: myös kidutuskammioon)
|
||||
help.k_slap = Shift+Vasen Käsi: läimäytä omia, mukiloi sankareita omalla maalla
|
||||
help.k_pan = Oikea veto Siirrä karttaa (myös WASD/nuolet, ruudun reuna)
|
||||
help.k_rot = Keskiveto Kierrä ja kallista kameraa (myös Q/E, R/F)
|
||||
help.k_reset = Keski 2x klikkaus Palauta kulma ja kierto (paikka säilyy)
|
||||
help.k_zoom = Hiiren rulla Zoomaa
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Valitse työkalu
|
||||
help.k_misc = P / Välilyönti Tauko M Ääni päälle/pois +/- Voimakkuus
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Mikä sinne putoaa, se palaa. Kivisillat ylittävät sen.
|
||||
tile.bridge = Puusilta
|
||||
tile.lavabridge = Kivisilta
|
||||
tile.mushroom = Jättisieni
|
||||
tile.mushroom_grown = Täysikasvuinen: valmis kaadettavaksi.
|
||||
tile.mushroom_growing = Kasvaa: {0}% - sen voi kaataa vasta täysikasvuisena.
|
||||
tile.grove = Sienilehto
|
||||
tile.grove_info = Kukaan ei voi vallata tätä sammalta. Täällä jättisienet kasvavat uudelleen.
|
||||
tile.tree = Puu
|
||||
tile.plant_info = Merkitse hakulla: piru kaataa sen {0} puusta.
|
||||
tile.drawbridge = Laskusilta
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Avoimet ottelut
|
||||
net.no_rooms = Ei avoimia otteluita.
|
||||
net.create = Luo ottelu
|
||||
net.map_size = Kartta {0}x{1} · {2} pelaajaa
|
||||
net.coop_mission = Yhteiskampanja: tehtävä {0} ({1})
|
||||
net.coop_create = Yhteiskampanja
|
||||
net.coop_editor = Editorikampanja
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = parantaa itseään kolmanneksella aiheuttamastaan vahingost
|
||||
unit.giant = Jättiläinen
|
||||
unit.fairy = Keijuvakooja
|
||||
unit.battlemage = Taistelumaagi
|
||||
unit.thief = Varas
|
||||
unit.monk = Munkki
|
||||
unit.ranger = Samooja
|
||||
unit.inquisitor = Inkvisiittori
|
||||
unit.ram = Muurinmurtaja
|
||||
unit.catapult = Katapultti
|
||||
msg.trap_wrecked = Sankarit romuttivat ansan: {0}.
|
||||
best.giant.text = Jättiläinen on se, mitä tapahtuu, kun ritari syö vihanneksensa kolmensadan vuoden ajan. Hidas, valtava, ja ovet ovat sille pelkkä ehdotus. | Se romuttaa ansat pelkästään seisomalla niiden vieressä. Kuvottavat tavat. Lyö sitä kaikella, mieluiten kaukaa.
|
||||
best.fairy.text = Ritarikunnan pieni ilmiantaja: hehkuva siivekäs mäkärä, joka lähetetään edeltä etsimään ansasi ja kertomaan kaikille. Kun se on nähnyt ansan, sankarit murskaavat sen. | Se myös sirottelee parantavaa pölyä sankarien päälle. Läimäise se ensin. Kahdesti, varmuuden vuoksi.
|
||||
best.battlemage.text = Maagi ritarikunnan kaavussa, joka pitää muureja henkilökohtaisena loukkauksena. Vahvistetut seinät halkeavat hänen tulipallojensa alla, eikä hän lakkaa puhumasta siitä. | Hauras lähietäisyydellä. Teräsmuurit ärsyttävät häntä eniten - hyvä.
|
||||
best.thief.text = Nopeat sormet, vielä nopeammat jalat. Hän ei tule taistelemaan: hän hiipii aarreaittaasi, täyttää säkkinsä ja juoksee takaisin portille. | Tapa hänet ennen kuin hän pääsee karkuun, niin kulta on taas sinun - korkojen kera.
|
||||
best.monk.text = Vuosien paasto ja rukous ovat tehneet hänestä kuuron taikuudellesi: salama, rutto ja halla valuvat hänestä kuin sade katolta. | Nyrkitkin sattuvat. Ota hänet vastaan teräksellä, kynsillä ja hampailla.
|
||||
best.ranger.text = Ritarikunnan metsämies, joka ampuu kaukaa ja jättää rautaisia ansoja käytäviisi. Ansaan astuva olento haavoittuu ja jää hetkeksi jumiin. | Ansat pysyvät, kunnes joku astuu niihin. Mieluummin koboldi kuin hiisisi.
|
||||
best.inquisitor.text = Hauras, mutta hänen saarnansa ovat vaarallisia: kahdenkymmenen sekunnin välein hän puhuu jonkin heikoimmista olennoistasi - koboldin tai hämähäkin - sankarien puolelle. | Vahvat olennot vain nauravat hänelle. Iske ensin.
|
||||
best.ram.text = Puunrunko rautakärjellä ja katto nuolia vastaan. Olentosi eivät sitä kiinnosta – ovesi, muurisi ja tornisi kyllä. | Pysäytä se ennen ovea tai hanki uusi ovi.
|
||||
best.catapult.text = Puinen varsi, ämpäri kiviä ja ritari, jolla on liikaa vapaa-aikaa. Se ampuu torneja kauempaa ja tähtää niihin ensin. | Ryntää sen kimppuun: läheltä se ei vahingoita kärpästäkään.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Rubiinisuoni
|
||||
ed.terrain.diamond = Timanttisuoni
|
||||
ed.terrain.steel = Teräsmuuri
|
||||
ed.terrain.mushroom = Jättisieni
|
||||
ed.terrain.grove = Sienilehto
|
||||
ed.terrain.grass = Ruoho (maan päällä)
|
||||
ed.terrain.tree = Puu
|
||||
ed.place.core = Sieluydin
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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ä).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Crookfinger: Ha! Kuulitko? Idästä tulee jotain. Valmistaudu: salama
|
||||
tut.obj6 = Kukista kaksi ritaria.
|
||||
tut.knights = Crookfinger: Kaksi ritaria! Lähetä olentosi hyökkäyslipulla (Käskyt) tai iske heitä salamalla.
|
||||
tut.win = Crookfinger: Hyvin tehty, mestari! Siinä kaikki. No - melkein. Kampanja kertoo loput. Panen teeveden kiehumaan.
|
||||
|
||||
# master talents
|
||||
cat.master = Mestari
|
||||
talent.title = Mestarin kyvyt
|
||||
talent.points = Vapaita kykypisteitä: {0}
|
||||
talent.next = {0}/{1} kokemusta seuraavaan pisteeseen
|
||||
talent.all = Kaikki kyvyt on opittu.
|
||||
talent.builder = Rakentaja
|
||||
talent.warlord = Sotaherra
|
||||
talent.warlock = Noita
|
||||
talent.merchant = Kauppias
|
||||
talent.builder.desc = Pirut kaivavat {0}% nopeammin, huoneet maksavat {1}% vähemmän.
|
||||
talent.warlord.desc = Olennot lyövät {0}% kovempaa ja ottavat {1}% vähemmän vahinkoa.
|
||||
talent.warlock.desc = Mana virtaa {0}% nopeammin, loitsut maksavat {1}% vähemmän.
|
||||
talent.merchant.desc = Palkat ovat {0}% pienemmät, toimitettu aarre on {1}% arvokkaampi.
|
||||
talent.none = Ei vielä opittu.
|
||||
talent.then = Seuraava taso: {0}
|
||||
msg.talent_point = Uusi kykypiste! Käytä se mestarin ikkunassa.
|
||||
msg.talent_learned = {0}: taso {1} opittu.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Selkeää
|
||||
weather.rain = Sade
|
||||
weather.fog = Sumu
|
||||
weather.storm = Myrsky
|
||||
msg.weather_clear = Maan päällä taivas selkenee.
|
||||
msg.weather_rain = Maan päällä alkaa sataa - tuli ei syty.
|
||||
msg.weather_fog = Sumu leviää maan ylle - ulkona kaikki näkevät vähemmän.
|
||||
msg.weather_storm = Maan päällä nousee myrsky! Salamat iskevät yksiköihin ja puihin.
|
||||
msg.lightning_unit = Salama iski: {0}!
|
||||
# creature names
|
||||
names.imp = Muru, Räkä, Kaivuri, Kura, Nöyhtä, Tönö, Myyrä, Lörppö, Nappula, Raapija, Möykky, Rapa, Sotku, Tuhru, Pikkis, Kuokka, Kärsä, Pökkelö, Läikkä, Moska
|
||||
names.kobold = Räksy, Nipistäjä, Hammas, Rupi, Haukku, Ruoste, Vips, Purija, Kitas, Rotta, Riepu, Piikki, Kynsi, Rähinä, Kivinen, Kikatus, Tönäisy, Kolho, Nirso, Naarmu
|
||||
names.spider = Kipitys, Villajalka, Hämis, Kutitus, Verkkonen, Kahdeksikko, Lanka, Kammotus, Näppis, Silkki, Kutoja, Hyppy, Nukkajalka, Tekla, Rulla, Näpsä, Kerä, Tahmis, Vipellys, Neulanen
|
||||
names.mage = Bartolomeus, Ignatius, Leopold, Teobald, Adalbert, Hieronymus, Valdemar, Gottfrid, Kornelius, Ottokar, Florentin, Engelbert, Pankratius, Rupertus, Konstantin, Eusebius, Severin, Barnabas, Arkibald, Vitus
|
||||
names.ogre = Möhkäle, Kökkö, Pläjäys, Mötikkä, Tömps, Tollo, Jöhkäle, Mörkö, Tallaaja, Puupää, Molskis, Pulla, Mahakas, Ähky, Nuija, Härkä, Rymy, Klöntti, Jytky, Rötkö
|
||||
names.jailer = Synkkä, Salpa, Kahle, Lukko, Tyrmä, Rautaparta, Rengas, Pultti, Ankara, Kalteri, Äreä, Koukku, Silmukka, Avain, Kolkko, Kilinä, Vartija, Tyrmänvahti, Häkki, Karvas
|
||||
names.priestess = Hehku, Liekki, Tuhka, Serafiina, Roihu, Tulia, Morwen, Brynhild, Ignia, Kipinä, Purppura, Säihky, Korppi, Lilith, Taula, Isolde, Palo, Nyx, Vesper, Loimu
|
||||
names.chainbreaker = Kahlerikko, Ulfgar, Rautasielu, Brannok, Kragmor, Valdrek, Oka, Gorath, Kahleeton, Rurik, Torvald, Hagen, Durnok, Sirpale, Lieska, Ulfrik, Korrun, Arvak, Sigurd, Halvard
|
||||
names.bat = Piip, Lepatus, Vinku, Suhina, Kaiku, Läpsy, Hämärä, Vihellys, Sirkutus, Hampaus, Pyörre, Lepakko, Koi, Piipero, Suhis, Roikku, Sametti, Livahdus, Kiljaisu, Salama
|
||||
names.gargoyle = Graniitti, Liuske, Basaltti, Kivikasvo, Laasti, Pikkukivi, Sora, Piikivi, Sauma, Raunio, Kvartsi, Hohkakivi, Reunus, Känsä, Kiviparta, Nyrpeä, Pääty, Vesisylki, Taltta, Sokkeli
|
||||
names.owl = Huuhkaja, Viisastelija, Sulkakynä, Bartoldus, Minerva, Tobias, Huhuu, Pöllölä, Sulkanen, Huokaus, Pohdiskelija, Vanhin, Lehtopöllö, Noctua, Pöllövaari, Pergamentti, Iltahämärä, Yöpöllö, Professori, Untuva
|
||||
names.wyrm = Vermithrax, Tuhkakita, Kärventäjä, Calderon, Ignivar, Hehkusarvi, Drakkar, Pyrrhus, Obsidiaani, Hiilikita, Vulkanor, Polttohammas, Malgrath, Tyrax, Inferno, Rikki, Kaldor, Fafnir, Ragnor, Hiillos
|
||||
names.cogling = Ratas, Hammasratas, Ruuvi, Räikkä, Pultti, Vaihde, Nikkari, Vempain, Avainen, Hammaspyörä, Kilkatus, Näpertäjä, Niitti, Surina, Kampi, Hyrrä, Laippa, Naksutin, Tiktak, Nippa
|
||||
names.revenant = Aamunkoi, Kauna, Harmaa, Valitus, Ontto, Suru, Kalvas, Murhe, Varjo, Multa, Kalmo, Hämy, Krypta, Kaiho, Valvoja, Kumpu, Käärinliina, Elegia, Sakka, Autio
|
||||
names.beast = Takku, Karvakasa, Bruno, Pörrö, Mussutus, Sammalselkä, Murina, Otso, Kontio, Rouske, Naksu, Kömpi, Harjas, Sammal, Jyrinä, Ohdake, Sirinä, Nurina, Nalle, Nuuskija
|
||||
names.human = Gerhard, Roland, Konrad, Aleksander, Tristan, Eskil, Gawain, Lancelot, Paavali, Hugo, Bertil, Gottfrid, Eerik, Sigfrid, Ragnar, Vilhelm, Kustaa, Gilbert, Rasmus, Birger
|
||||
epithets.mid = Luunmurskaaja, Mutajalka, Rotanpurija, Likanaama, Kivenjyrsijä, Kallonkopauttaja, Haisunyrkki, Ruostenivel, Mahanmätkijä, Säärenpotkija, Homeparta, Likakynsi, Räkälinko, Korvanjyrsijä, Polvenmurtaja, Madonsyöjä
|
||||
epithets.high = Kuninkaansurma, Tuomionkoura, Kauhuhammas, Maailmanmurskaaja, Sankarinsurma, Myrskynyrkki, Yönkauhu, Sielunniittäjä, Rautasydän, Linnanmurskaaja, Lohikäärmeensurma, Helvetinkulkija, Varjoruhtinas, Verikruunu, Tuomiontulva, Tähdenmurskaaja
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Vrata se urušavaju! Pojavljuje se Čuvar vrata s braniteljima (
|
||||
msg.gate_defenders = Vrata dozivaju branitelje: {0}!
|
||||
msg.level_up = {0} doseže razinu {1}.
|
||||
msg.hero_slain = Poražen: {0}! Plijen: {1} zlata.
|
||||
msg.thief_in_treasury = Lopov je u tvojoj riznici!
|
||||
msg.thief_escaped = Lopov je pobjegao s {0} zlata!
|
||||
msg.snare_caught = Stalo je u zamku: {0}!
|
||||
msg.converted = Inkvizitor je preobratio jedno tvoje stvorenje: {0}!
|
||||
msg.creature_died = Palo je tvoje stvorenje: {0}.
|
||||
msg.portal_claimed = Portal je tvoj! Privlači stvorenja.
|
||||
msg.flag_removed = Zastava napada uklonjena. Stvorenja se vraćaju.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Nema dovoljno mane (potrebno {0}).
|
||||
hint.portal_no_lair = Portal je otvoren, ali bez brloga nijedno stvorenje neće doći.
|
||||
hint.no_farm = Stvorenja žele jesti: izgradi vrt gljiva.
|
||||
hint.lair_full = Svi kreveti u brlogu su zauzeti. Proširi brlog.
|
||||
hint.creature_max = Tvoja tamnica ima 100 stvorenja - kroz portal ih više ne dolazi.
|
||||
hint.gate_attack = Tvoja stvorenja napadaju Vrata junaka!
|
||||
hint.treasury_full = Riznice su pune! Izgradi još polja riznice.
|
||||
hint.flag_unreachable = Tvoja stvorenja ne mogu doći do zastave napada.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Desni klik Spusti jedinicu (zarobljenike i u mučionicu)
|
||||
help.k_slap = Shift+lijevi Ruka: pljusni svoje jedinice, udari junake na svom tlu
|
||||
help.k_pan = Desno povlačenje Pomiči kartu (i WASD/strelice, rub zaslona)
|
||||
help.k_rot = Srednje povlačenje Okreći i naginji kameru (i Q/E, R/F)
|
||||
help.k_reset = Srednji 2x klik Vrati kut i okret (mjesto ostaje)
|
||||
help.k_zoom = Kotačić miša Zumiranje
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Odabir alata
|
||||
help.k_misc = P / Razmak Stanka M Zvuk uklj./isklj. +/- Glasnoća
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Što god upadne, izgori. Kameni mostovi je premošćuju.
|
||||
tile.bridge = Drveni most
|
||||
tile.lavabridge = Kameni most
|
||||
tile.mushroom = Divovska gljiva
|
||||
tile.mushroom_grown = Odrasla: spremna za sječu.
|
||||
tile.mushroom_growing = Raste: {0}% - može se posjeći tek kad odraste.
|
||||
tile.grove = Gljivarski gaj
|
||||
tile.grove_info = Nitko ne može osvojiti ovu mahovinu. Ovdje ponovno rastu divovske gljive.
|
||||
tile.tree = Stablo
|
||||
tile.plant_info = Označi krampom: vražić je sruši za {0} drva.
|
||||
tile.drawbridge = Pokretni most
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Otvorene partije
|
||||
net.no_rooms = Nema otvorenih partija.
|
||||
net.create = Nova partija
|
||||
net.map_size = Karta {0}x{1} · {2} igrača
|
||||
net.coop_mission = Co-op kampanja: misija {0} ({1})
|
||||
net.coop_create = Co-op kampanja
|
||||
net.coop_editor = Kampanja uređivača
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = liječi se za trećinu štete koju nanese.
|
||||
unit.giant = Div
|
||||
unit.fairy = Vila uhoda
|
||||
unit.battlemage = Bojni mag
|
||||
unit.thief = Lopov
|
||||
unit.monk = Redovnik
|
||||
unit.ranger = Lovac
|
||||
unit.inquisitor = Inkvizitor
|
||||
unit.ram = Ovan
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Junaci su uništili zamku: {0}.
|
||||
best.giant.text = Div je ono što se dogodi kad vitez tristo godina jede povrće. Spor, golem, a vrata smatra prijedlogom. | Zamke uništava samim tim što stoji pokraj njih. Odvratno ponašanje. Udri ga svime, najbolje izdaleka.
|
||||
best.fairy.text = Mali doušnik Reda: svjetleća mušica s krilima, poslana naprijed da pronađe tvoje zamke i svima ih oda. Kad jednom vidi zamku, junaci je razbiju. | Usto posipa junake ljekovitim prahom. Zgnječi je prvu. Dvaput, za svaki slučaj.
|
||||
best.battlemage.text = Mag u halji Reda koji zidove smatra osobnom uvredom. Ojačani zidovi pucaju pod njegovim vatrenim kuglama, a on o tome neprestano priča. | Krhak izbliza. Čelični zidovi najviše ga živciraju - dobro.
|
||||
best.thief.text = Brzi prsti, još brže noge. Ne dolazi se boriti: ušulja se u tvoju riznicu, napuni vreću i otrči natrag do vrata. | Ubij ga prije nego pobjegne i zlato je opet tvoje - s kamatama.
|
||||
best.monk.text = Godine posta i molitve učinile su ga gluhim za tvoju magiju: munja, kuga i mraz klize s njega kao kiša s krova. | I šake bole. Dočekaj ga čelikom, pandžama i zubima.
|
||||
best.ranger.text = Šumar Reda koji puca izdaleka i ostavlja željezne zamke u tvojim hodnicima. Stvorenje koje stane u jednu ranjeno je i neko vrijeme zaglavljeno. | Zamke ostaju dok netko ne stane u njih. Bolje kobold nego tvoj ogr.
|
||||
best.inquisitor.text = Krhak, ali su mu propovijedi opasne: svakih dvadeset sekundi nagovori jedno od tvojih najslabijih stvorenja - kobolda ili pauka - da prijeđe junacima. | Jaka stvorenja mu se samo smiju. Udari prvi.
|
||||
best.ram.text = Deblo sa željeznom kapom i krovom protiv strijela. Tvoja stvorenja ga ne zanimaju – tvoja vrata, zidovi i tornjevi da. | Zaustavi ga prije vrata ili nabavi nova vrata.
|
||||
best.catapult.text = Drvena ruka, kanta kamenja i vitez s previše slobodnog vremena. Gađa dalje od tvojih tornjeva i cilja ih prve. | Jurni na nj: izbliza ni muhi ne nanosi zlo.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Žila rubina
|
||||
ed.terrain.diamond = Žila dijamanata
|
||||
ed.terrain.steel = Čelični zid
|
||||
ed.terrain.mushroom = Divovska gljiva
|
||||
ed.terrain.grove = Gljivarski gaj
|
||||
ed.terrain.grass = Trava (površina)
|
||||
ed.terrain.tree = Stablo
|
||||
ed.place.core = Jezgra duše
|
||||
@@ -1354,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č
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Krivoprst: Ha! Jesi li čuo? Nešto dolazi s istoka. Pripremi se: mun
|
||||
tut.obj6 = Porazi dva viteza.
|
||||
tut.knights = Krivoprst: Dva viteza! Pošalji stvorenja zastavom napada (Naredbe) ili ih udari munjom.
|
||||
tut.win = Krivoprst: Bravo, gospodaru! To je sve. Pa - skoro. Ostatak će ispričati kampanja. Idem staviti vodu za čaj.
|
||||
|
||||
# master talents
|
||||
cat.master = Gospodar
|
||||
talent.title = Gospodarevi talenti
|
||||
talent.points = Slobodni bodovi talenta: {0}
|
||||
talent.next = {0}/{1} iskustva do sljedećeg boda
|
||||
talent.all = Svi talenti su naučeni.
|
||||
talent.builder = Graditelj
|
||||
talent.warlord = Vojskovođa
|
||||
talent.warlock = Vještac
|
||||
talent.merchant = Trgovac
|
||||
talent.builder.desc = Vražići kopaju {0}% brže, sobe koštaju {1}% manje.
|
||||
talent.warlord.desc = Stvorenja udaraju {0}% jače i primaju {1}% manje štete.
|
||||
talent.warlock.desc = Mana teče {0}% brže, čarolije koštaju {1}% manje.
|
||||
talent.merchant.desc = Plaće su {0}% niže, dostavljeno blago vrijedi {1}% više.
|
||||
talent.none = Još nije naučeno.
|
||||
talent.then = Sljedeća razina: {0}
|
||||
msg.talent_point = Novi bod talenta! Potroši ga u prozoru gospodara.
|
||||
msg.talent_learned = {0}: naučena razina {1}.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Vedro
|
||||
weather.rain = Kiša
|
||||
weather.fog = Magla
|
||||
weather.storm = Oluja
|
||||
msg.weather_clear = Na površini se razvedrava.
|
||||
msg.weather_rain = Na površini počinje kiša - vatra se ne hvata.
|
||||
msg.weather_fog = Magla se spušta nad zemlju - vani svi vide manje.
|
||||
msg.weather_storm = Na površini se sprema oluja! Munje pogađaju jedinice i stabla.
|
||||
msg.lightning_unit = Munja je pogodila: {0}!
|
||||
# creature names
|
||||
names.imp = Mrvica, Šmrkljo, Kopko, Blatko, Prljko, Čupko, Rovko, Gnjecko, Smrdko, Grebko, Kvrga, Brlja, Mazalo, Packo, Grudica, Lopatko, Njuško, Kopuša, Mrljko, Šljampa
|
||||
names.kobold = Kevko, Štipko, Zubonja, Šugo, Grizo, Hrđo, Fićo, Ugrizić, Zjalo, Štakor, Krpo, Bodljo, Kandžo, Gnjavko, Korijen, Kikotko, Gurko, Zarez, Krcko, Ogrebo
|
||||
names.spider = Gmizalica, Vunko, Paučić, Škakljo, Mrežica, Osmonoga, Nitkica, Jezica, Prčko, Svilenka, Tkalja, Skokić, Dlakavko, Tekla, Kalemko, Hvatko, Klupko, Ljepko, Vrtko, Iglica
|
||||
names.mage = Bartolomej, Ignacije, Leopold, Teobald, Adalbert, Jeronim, Valdemar, Gotfrid, Kornelije, Otokar, Florijan, Engelbert, Pankracije, Ruprecht, Konstantin, Euzebije, Severin, Barnaba, Arčibald, Vitomir
|
||||
names.ogre = Gromo, Klada, Bumbo, Grudo, Tupko, Glupko, Tresko, Batina, Gazilo, Tikvan, Pljus, Debeljko, Buhto, Trbonja, Toljaga, Volina, Tutnjo, Kvrgo, Bubanj, Gorbo
|
||||
names.jailer = Mrki, Zasun, Okov, Lokot, Tamnica, Željezobrad, Karika, Svornjak, Strogi, Rešetko, Namrgođen, Kuka, Omča, Ključar, Mračni, Zvekan, Klada, Stražar, Kavez, Gorki
|
||||
names.priestess = Žar, Plamena, Pepela, Serafina, Buktinja, Vatrena, Morana, Brunhilda, Ignija, Iskra, Grimizna, Žarka, Gavranka, Lilit, Trud, Izolda, Ognjena, Nyx, Večernja, Plamenka
|
||||
names.chainbreaker = Lomilanac, Ulfgar, Željezduh, Branok, Kragmor, Valdrek, Trn, Gorath, Nesputani, Rurik, Torvald, Hagen, Durnok, Krhotina, Plamen, Vukoslav, Korun, Arvak, Oslobodilac, Halvard
|
||||
names.bat = Pijuk, Lepršo, Ciko, Šušur, Jeka, Mahalica, Sumrak, Zviždo, Cvrčko, Zubić, Vrtlog, Šišmišić, Moljac, Pijukić, Šušo, Visko, Baršun, Šmugo, Ciča, Munja
|
||||
names.gargoyle = Granit, Škriljac, Bazalt, Kamenko, Malter, Šljunko, Tucanik, Kremen, Fuga, Ruševina, Kvarc, Plovućac, Vijenac, Kvrgo, Kamenobrad, Mrzovolja, Zabat, Rigalo, Dlijeto, Postolje
|
||||
names.owl = Ćuk, Mudrica, Pero, Bartold, Minerva, Tobija, Huhu, Sovica, Perić, Uzdah, Mislilac, Starješina, Jejina, Noctua, Sovoslav, Pergamena, Suton, Ušara, Profesor, Paperje
|
||||
names.wyrm = Vermitraks, Pepeljavi, Palež, Kalderon, Ignivar, Žarorog, Drakar, Pir, Opsidijan, Žaroust, Vulkanor, Palizub, Malgrat, Tiraks, Inferno, Sumpor, Kaldor, Fafnir, Ragnor, Ugarak
|
||||
names.cogling = Zupčanik, Kotačić, Vijak, Čegrtaljka, Zakovica, Prijenos, Majstorko, Spravica, Ključić, Pogon, Zveckalo, Petljo, Klinčić, Zujo, Ručica, Zvrk, Prirubnica, Škljoc, Tiktak, Navojko
|
||||
names.revenant = Sjeta, Gorčina, Pepeljko, Jauk, Šupalj, Tuga, Blijedi, Jad, Sjena, Prah, Grobar, Sumrak, Kripta, Čežnja, Bdijenje, Humak, Pokrov, Elegija, Talog, Pustoš
|
||||
names.beast = Čupavko, Medo, Bruno, Rundo, Mljacko, Mahovinko, Mrmljo, Medvjedić, Grmko, Hrsko, Škljocko, Gmizo, Čekinjo, Mahovina, Tutanj, Kupina, Cvrčo, Gunđalo, Medonja, Njuškalo
|
||||
names.human = Gerard, Roland, Konrad, Albert, Tristan, Eckhart, Gavin, Lancelot, Parsifal, Hugo, Bertram, Gotfrid, Ortvin, Balduin, Rajnold, Vilfrid, Dietrich, Gilbert, Ruprecht, Dragutin
|
||||
epithets.mid = Lomikost, Blatonog, Štakorogriz, Prljolica, Kamenožder, Lubanjolom, Smrdošaka, Hrđozglob, Trbuhoboj, Goljenoboj, Pljesnobrad, Prljokandža, Šmrkljobacač, Uhogriz, Koljenolom, Crvožder
|
||||
epithets.high = Kraljoubica, Kobošaka, Strahozub, Svjetolom, Junakomor, Olujošaka, Noćnostrah, Dušožetelac, Željezosrce, Gradolom, Zmajoubica, Paklohod, Sjenovladar, Krvokrun, Kobnoplima, Zvjezdolom
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = A kapu omladozik! Megjelent a Kapuőr {0} védelmezővel!
|
||||
msg.gate_defenders = A kapu {0} védelmezőt idéz!
|
||||
msg.level_up = {0} elérte ezt a szintet: {1}.
|
||||
msg.hero_slain = Legyőzve: {0}! Zsákmány: {1} arany.
|
||||
msg.thief_in_treasury = Egy tolvaj van a kincstáradban!
|
||||
msg.thief_escaped = Egy tolvaj megszökött {0} arannyal!
|
||||
msg.snare_caught = Csapdába lépett: {0}!
|
||||
msg.converted = Az inkvizítor megtérítette egyik lényedet: {0}!
|
||||
msg.creature_died = Elesett: {0}.
|
||||
msg.portal_claimed = Tiéd a portál! Vonzza a lényeket.
|
||||
msg.flag_removed = Támadási zászló eltávolítva. A lényeid visszatérnek.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Nincs elég mana ({0} kell).
|
||||
hint.portal_no_lair = A portál nyitva van, de odú nélkül egy lény sem jön.
|
||||
hint.no_farm = A lények enni akarnak: építs gombakertet.
|
||||
hint.lair_full = Az odúban minden ágy foglalt. Bővítsd az odút.
|
||||
hint.creature_max = A kazamatádban 100 lény él - több nem jön a portálon át.
|
||||
hint.gate_attack = A lényeid megtámadták a Hőskaput!
|
||||
hint.treasury_full = A kincstáraid tele vannak! Építs további kincstármezőket.
|
||||
hint.flag_unreachable = A lényeid nem érik el a támadási zászlót.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Jobb klikk Egység lerakása (foglyok: a kínzókamrába is)
|
||||
help.k_slap = Shift+Bal Kéz: saját egységek felpofozása, hősök ütése a saját földeden
|
||||
help.k_pan = Jobb húzás Térkép mozgatása (WASD/nyilak, képernyőszél is)
|
||||
help.k_rot = Középső húzás Kamera forgatása és döntése (Q/E, R/F is)
|
||||
help.k_reset = Középső 2x katt. Szög és forgatás alaphelyzetbe (a hely marad)
|
||||
help.k_zoom = Egérgörgő Nagyítás
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Eszköz kiválasztása
|
||||
help.k_misc = P / Szóköz Szünet M Hang be/ki +/- Hangerő
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Ami beleesik, elég. Kőhidak vezetnek át rajta.
|
||||
tile.bridge = Fahíd
|
||||
tile.lavabridge = Kőhíd
|
||||
tile.mushroom = Óriásgomba
|
||||
tile.mushroom_grown = Kifejlett: kivágható.
|
||||
tile.mushroom_growing = Nő: {0}% - csak kifejletten lehet kivágni.
|
||||
tile.grove = Gombaliget
|
||||
tile.grove_info = Ezt a mohát senki sem foglalhatja el. Itt újra óriásgombák nőnek.
|
||||
tile.tree = Fa
|
||||
tile.plant_info = Jelöld meg a csákánnyal: egy manó kivágja {0} fáért.
|
||||
tile.drawbridge = Felvonóhíd
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Nyitott meccsek
|
||||
net.no_rooms = Nincsenek nyitott meccsek.
|
||||
net.create = Új meccs
|
||||
net.map_size = Térkép {0}x{1} · {2} játékos
|
||||
net.coop_mission = Kooperatív hadjárat: {0}. küldetés ({1})
|
||||
net.coop_create = Koop hadjárat
|
||||
net.coop_editor = Saját hadjárat
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = az okozott sebzés harmadával gyógyítja magát.
|
||||
unit.giant = Óriás
|
||||
unit.fairy = Tündérkém
|
||||
unit.battlemage = Harci mágus
|
||||
unit.thief = Tolvaj
|
||||
unit.monk = Szerzetes
|
||||
unit.ranger = Vadőr
|
||||
unit.inquisitor = Inkvizítor
|
||||
unit.ram = Faltörő kos
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = A hősök tönkretettek egy csapdát: {0}.
|
||||
best.giant.text = Az óriás az, ami akkor lesz, ha egy lovag háromszáz évig megeszi a zöldséget. Lassú, hatalmas, és az ajtókat csak javaslatnak tekinti. | Pusztán azzal tönkreteszi a csapdákat, hogy melléjük áll. Visszataszító modor. Vágj hozzá mindent, lehetőleg messziről.
|
||||
best.fairy.text = A Rend kis besúgója: egy világító szúnyog szárnyakkal, akit előreküldenek, hogy megtalálja a csapdáidat, és mindenkinek elmondja. Ha egyszer meglátott egy csapdát, a hősök szétverik. | Ráadásul gyógyító port szór a hősökre. Csapd agyon elsőként. Kétszer, a biztonság kedvéért.
|
||||
best.battlemage.text = Mágus a Rend köntösében, aki a falakat személyes sértésnek veszi. A megerősített falak szétrobbannak a tűzgolyói alatt, és sosem hallgat el róla. | Közelről törékeny. Az acélfalak bosszantják a legjobban - helyes.
|
||||
best.thief.text = Fürge ujjak, még fürgébb lábak. Nem harcolni jön: beoson a kincstáradba, megtölti a zsákját, és visszafut a kapuhoz. | Öld meg, mielőtt elszökik, és az arany újra a tiéd - kamatostul.
|
||||
best.monk.text = Évnyi böjt és ima süketté tette a mágiádra: a villám, a pestis és a fagy úgy pereg le róla, mint eső a tetőről. | Az ököl is fáj. Fogadd acéllal, karmokkal és fogakkal.
|
||||
best.ranger.text = A Rend erdőjárója, aki messziről lő, és vascsapdákat hagy a folyosóidon. Az a lény, amelyik belelép, megsebesül, és egy ideig beragad. | A csapdák ott maradnak, amíg valaki bele nem lép. Inkább egy kobold, mint az ogréd.
|
||||
best.inquisitor.text = Törékeny, de a prédikációi veszélyesek: húszmásodpercenként rábeszéli egyik leggyengébb lényedet - egy koboldot vagy egy pókot -, hogy álljon a hősök oldalára. | Az erős lények csak kinevetik. Üss te először.
|
||||
best.ram.text = Egy vaskupakos fatörzs, nyilak ellen tetővel. A lényeid hidegen hagyják – az ajtóid, falaid és tornyaid nem. | Állítsd meg az ajtó előtt, vagy szerezz új ajtót.
|
||||
best.catapult.text = Egy fakar, egy vödör kő és egy lovag túl sok szabadidővel. Messzebbre lő a tornyaidnál, és először azokat veszi célba. | Rohamozd meg: közelről a légynek sem árt.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Rubinér
|
||||
ed.terrain.diamond = Gyémántér
|
||||
ed.terrain.steel = Acélfal
|
||||
ed.terrain.mushroom = Óriásgomba
|
||||
ed.terrain.grove = Gombaliget
|
||||
ed.terrain.grass = Fű (felszín)
|
||||
ed.terrain.tree = Fa
|
||||
ed.place.core = Lélekmag
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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:
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Crookfinger: Ha! Hallottad? Valami jön keletről. Készülj: a vill
|
||||
tut.obj6 = Győzd le a két lovagot.
|
||||
tut.knights = Crookfinger: Két lovag! Küldd rájuk a lényeidet a támadózászlóval (Parancs), vagy sújtsd őket villámmal.
|
||||
tut.win = Crookfinger: Szép munka, mester! Ennyi az egész. Nos - majdnem. A többit elmondja a hadjárat. Felteszem a teavizet.
|
||||
|
||||
# master talents
|
||||
cat.master = Mester
|
||||
talent.title = Mesteri tehetségek
|
||||
talent.points = Szabad tehetségpontok: {0}
|
||||
talent.next = {0}/{1} tapasztalat a következő pontig
|
||||
talent.all = Minden tehetség megtanulva.
|
||||
talent.builder = Építész
|
||||
talent.warlord = Hadúr
|
||||
talent.warlock = Boszorkánymester
|
||||
talent.merchant = Kereskedő
|
||||
talent.builder.desc = A manók {0}%-kal gyorsabban ásnak, a szobák {1}%-kal olcsóbbak.
|
||||
talent.warlord.desc = A lények {0}%-kal erősebben ütnek, és {1}%-kal kevesebb sebzést kapnak.
|
||||
talent.warlock.desc = A mana {0}%-kal gyorsabban áramlik, a varázslatok {1}%-kal olcsóbbak.
|
||||
talent.merchant.desc = A bérek {0}%-kal alacsonyabbak, a leadott kincs {1}%-kal többet ér.
|
||||
talent.none = Még nincs megtanulva.
|
||||
talent.then = Következő szint: {0}
|
||||
msg.talent_point = Új tehetségpont! Költsd el a mester ablakában.
|
||||
msg.talent_learned = {0}: {1}. szint megtanulva.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Derült
|
||||
weather.rain = Eső
|
||||
weather.fog = Köd
|
||||
weather.storm = Vihar
|
||||
msg.weather_clear = A felszínen kitisztul az ég.
|
||||
msg.weather_rain = A felszínen esni kezd - a tűz nem kap lángra.
|
||||
msg.weather_fog = Köd ereszkedik a tájra - odakint mindenki kevesebbet lát.
|
||||
msg.weather_storm = A felszínen vihar készül! Villámok csapnak egységekbe és fákba.
|
||||
msg.lightning_unit = Villám sújtotta: {0}!
|
||||
# creature names
|
||||
names.imp = Morzsa, Takony, Ásóka, Sarasló, Kosz, Pöttöm, Turkáló, Maszat, Csipa, Kaparó, Rög, Pacni, Dagonya, Piszok, Göröngy, Lapátka, Szutyok, Mütyür, Paca, Kukac
|
||||
names.kobold = Csahos, Csípő, Fogas, Rühes, Harapós, Rozsdás, Suhanc, Marcangó, Bámész, Patkány, Rongyos, Tüske, Karmos, Kötekedő, Gyökér, Vihogó, Lökdöső, Csorba, Reccs, Karcos
|
||||
names.spider = Mászka, Gyapjúláb, Pókocska, Csiklandó, Hálóka, Nyolclábú, Fonalka, Borzi, Matató, Selyemke, Szövőke, Ugri, Bolyhos, Tekla, Orsóka, Kapkodó, Gombolyag, Ragacs, Pörgő, Tűcske
|
||||
names.mage = Bertalan, Ignác, Lipót, Tivadar, Adalbert, Jeromos, Valdemár, Gotfrid, Kornél, Otokár, Flórián, Engelbert, Pongrác, Ruprecht, Konstantin, Özséb, Szilveszter, Barnabás, Archibald, Bonifác
|
||||
names.ogre = Döbrögi, Tuskó, Bumm, Szikla, Tökfej, Fatuskó, Puffancs, Püfölő, Dübörgő, Fafej, Pottyan, Pocak, Bunkó, Hasas, Dorong, Ökör, Robaj, Darab, Dob, Gorbó
|
||||
names.jailer = Komor, Retesz, Bilincs, Lakat, Tömlöc, Vasszakáll, Karika, Csavar, Szigorú, Rácsos, Mogorva, Kampó, Hurok, Kulcsár, Sötét, Csörgő, Kaloda, Porkoláb, Kalitka, Keserű
|
||||
names.priestess = Parázs, Lángi, Hamvas, Szeráfina, Máglya, Tüzes, Morvenna, Brünhilda, Ignia, Szikra, Bíbor, Zsarátnok, Hollóka, Lilit, Tapló, Izolda, Lobogó, Nyx, Esthajnal, Lángocska
|
||||
names.chainbreaker = Láncszaggató, Ulfgár, Vaslélek, Brannok, Kragmor, Valdrek, Tövis, Gorath, Kötetlen, Rurik, Torvald, Hágen, Durnok, Szilánk, Lángoló, Farkasvér, Korrun, Arvak, Szabadító, Halvard
|
||||
names.bat = Cincogó, Rebbenő, Pitty, Suhogó, Visszhang, Csapkodó, Alkony, Fütyülő, Ciripelő, Fogacska, Örvény, Denevérke, Moly, Pittyegő, Suhi, Lógó, Bársony, Surranó, Visító, Villám
|
||||
names.gargoyle = Gránit, Pala, Bazalt, Kőarc, Habarcs, Kavics, Murva, Kovakő, Fuga, Rom, Kvarc, Horzsakő, Párkány, Bütyök, Kőszakáll, Morcos, Orom, Vízköpő, Véső, Lábazat
|
||||
names.owl = Uhu, Okoska, Lúdtoll, Bartold, Minerva, Tóbiás, Huhú, Baglyocska, Tollacska, Sóhaj, Tűnődő, Öregúr, Kuvik, Noctua, Bagolybert, Pergamen, Szürkület, Macskabagoly, Professzor, Pihe
|
||||
names.wyrm = Vermithrax, Hamutorok, Perzselő, Kalderon, Ignivar, Izzószarv, Drakkar, Pürrhosz, Obszidián, Parázsszáj, Vulkanor, Égetőfog, Malgrath, Tyrax, Infernó, Kénkő, Kaldor, Fafnir, Ragnor, Zsarát
|
||||
names.cogling = Fogaskerék, Kerekecske, Csavarka, Racsni, Szegecs, Áttétel, Ezermester, Kütyü, Kulcsocska, Lendkerék, Csilingelő, Piszmogó, Anyácska, Zümmögő, Forgattyú, Búgócsiga, Karima, Kattanó, Tiktak, Csavarhúzó
|
||||
names.revenant = Bánat, Keserv, Hamuszürke, Jajszó, Üresség, Gyász, Sápadt, Búbánat, Árny, Por, Sírásó, Homály, Kripta, Sóvárgás, Virrasztó, Halom, Szemfedő, Elégia, Üledék, Pusztaság
|
||||
names.beast = Bozont, Mackó, Brúnó, Csapzott, Csámcsogó, Mohahát, Morgó, Medvebocs, Bokor, Ropogó, Kattogó, Mászó, Sörte, Moha, Dörmögő, Szeder, Ciripelő, Zsörtölő, Brummogó, Szimatoló
|
||||
names.human = Gellért, Roland, Konrád, Albert, Trisztán, Ekhárt, Gawain, Lancelot, Parszifál, Hugó, Bertrám, Gotfrid, Örs, Baldvin, Rajnald, Vilfrid, Detre, Gilbert, Rupert, Bulcsú
|
||||
epithets.mid = Csonttörő, Sárlábú, Patkányrágó, Koszosképű, Kavicsrágó, Koponyapüfölő, Büdösöklű, Rozsdabütykű, Hasbavágó, Sípcsontrúgó, Penészszakáll, Koszkarmú, Takonyhajító, Fülrágó, Térdtörő, Kukacevő
|
||||
epithets.high = Királyölő, Végzetkéz, Rémfog, Világromboló, Hősveszejtő, Viharököl, Éjrém, Lélekarató, Vasszív, Várromboló, Sárkányölő, Pokoljáró, Árnyúr, Vérkoronás, Vészár, Csillagtörő
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = La porta si sgretola! Appare il Guardiano della Porta con {0} di
|
||||
msg.gate_defenders = La porta evoca {0} difensori!
|
||||
msg.level_up = {0} raggiunge il livello {1}.
|
||||
msg.hero_slain = {0} sconfitto! Bottino: {1} oro.
|
||||
msg.thief_in_treasury = C'è un ladro nella tua tesoreria!
|
||||
msg.thief_escaped = Un ladro è scappato con {0} oro!
|
||||
msg.snare_caught = È finito in una tagliola: {0}!
|
||||
msg.converted = L'inquisitore ha convertito una delle tue creature: {0}!
|
||||
msg.creature_died = È caduto in battaglia: {0}.
|
||||
msg.portal_claimed = Il portale è tuo! Attira le creature.
|
||||
msg.flag_removed = Bandiera d'attacco rimossa. Le creature tornano indietro.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Mana insufficiente (servono {0}).
|
||||
hint.portal_no_lair = Il portale è aperto, ma senza una tana non verrà nessuna creatura.
|
||||
hint.no_farm = Le creature vogliono mangiare: costruisci una fungaia.
|
||||
hint.lair_full = Tutti i letti della tana sono occupati. Ingrandisci la tana.
|
||||
hint.creature_max = Il tuo dungeon ospita 100 creature: dal portale non ne arrivano altre.
|
||||
hint.gate_attack = Le tue creature attaccano la Porta degli Eroi!
|
||||
hint.treasury_full = Le tesorerie sono piene! Costruisci altre caselle di tesoreria.
|
||||
hint.flag_unreachable = Le tue creature non riescono a raggiungere la bandiera d'attacco.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Clic destro Posa un'unità (prigionieri: anche nella camera d
|
||||
help.k_slap = Shift+Sinistro Mano: schiaffo alle tue unità, pugno agli eroi sul tuo suolo
|
||||
help.k_pan = Destro+trascina Sposta la mappa (anche WASD/frecce, bordo schermo)
|
||||
help.k_rot = Centro+trascina Ruota e inclina la camera (anche Q/E, R/F)
|
||||
help.k_reset = Centro 2x clic Ripristina angolo e rotazione (il posto resta)
|
||||
help.k_zoom = Rotellina Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Scegli strumento
|
||||
help.k_misc = P / Spazio Pausa M Audio sì/no +/- Volume
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Ciò che ci cade brucia. I ponti di pietra la attraversano.
|
||||
tile.bridge = Ponte di legno
|
||||
tile.lavabridge = Ponte di pietra
|
||||
tile.mushroom = Fungo gigante
|
||||
tile.mushroom_grown = Cresciuto: pronto per il taglio.
|
||||
tile.mushroom_growing = Cresce: {0}% - si può abbattere solo quando è cresciuto.
|
||||
tile.grove = Boschetto di funghi
|
||||
tile.grove_info = Nessuno può conquistare questo muschio. Qui ricrescono funghi giganti.
|
||||
tile.tree = Albero
|
||||
tile.plant_info = Segnalo col piccone: un diavoletto lo abbatte per {0} legno.
|
||||
tile.drawbridge = Ponte levatoio
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Partite aperte
|
||||
net.no_rooms = Nessuna partita aperta.
|
||||
net.create = Crea partita
|
||||
net.map_size = Mappa {0}x{1} · {2} giocatori
|
||||
net.coop_mission = Campagna co-op: missione {0} ({1})
|
||||
net.coop_create = Campagna co-op
|
||||
net.join = Entra
|
||||
@@ -1075,12 +1088,20 @@ ability.drain.desc = si cura di un terzo del danno che infligge.
|
||||
unit.giant = Gigante
|
||||
unit.fairy = Fata spia
|
||||
unit.battlemage = Mago da battaglia
|
||||
unit.thief = Ladro
|
||||
unit.monk = Monaco
|
||||
unit.ranger = Ranger
|
||||
unit.inquisitor = Inquisitore
|
||||
unit.ram = Ariete
|
||||
unit.catapult = Catapulta
|
||||
msg.trap_wrecked = Gli eroi hanno distrutto una trappola: {0}.
|
||||
best.giant.text = Un gigante è ciò che succede quando un cavaliere mangia la verdura per trecento anni. Lento, enorme, e considera le porte un semplice suggerimento. | Distrugge le trappole solo stando loro accanto. Maniere rivoltanti. Colpiscilo con tutto, preferibilmente da lontano.
|
||||
best.fairy.text = La piccola spia dell'Ordine: un moscerino luminoso con le ali, mandato avanti a trovare le tue trappole e a dirlo a tutti. Appena ha visto una trappola, gli eroi la distruggono. | Sparge anche polvere curativa sugli eroi. Schiacciala per prima. Due volte, per sicurezza.
|
||||
best.battlemage.text = Un mago con la tonaca dell'Ordine che considera i muri un insulto personale. I muri rinforzati scoppiano sotto le sue palle di fuoco, e lui non smette mai di vantarsene. | Fragile da vicino. I muri d'acciaio lo irritano più di tutto - bene.
|
||||
best.thief.text = Dita svelte, piedi ancora più svelti. Non viene per combattere: si infila nella tua tesoreria, riempie il sacco e corre di nuovo alla porta. | Uccidilo prima che scappi e l'oro torna tuo - con gli interessi.
|
||||
best.monk.text = Anni di digiuno e preghiera lo hanno reso sordo alla tua magia: fulmine, peste e gelo gli scivolano addosso come pioggia su un tetto. | Anche i pugni fanno male. Accoglilo con acciaio, artigli e denti.
|
||||
best.ranger.text = Un uomo dei boschi dell'Ordine che tira da lontano e lascia tagliole di ferro nei tuoi corridoi. La creatura che ci mette il piede resta ferita e bloccata per un po'. | Le tagliole restano finché qualcuno non ci finisce dentro. Meglio un coboldo che il tuo orco.
|
||||
best.inquisitor.text = Fragile, ma le sue prediche sono pericolose: ogni venti secondi convince una delle tue creature più deboli - un coboldo o un ragno - a passare dalla parte degli eroi. | Le creature forti gli ridono in faccia. Colpisci per primo.
|
||||
best.ram.text = Un tronco con punta di ferro e un tetto contro le frecce. Delle tue creature non gli importa: solo di porte, muri e torri. | Fermalo prima che arrivi alla porta, o procurati una porta nuova.
|
||||
best.catapult.text = Un braccio di legno, un secchio di sassi e un cavaliere con troppo tempo libero. Tira più lontano delle tue torri e le punta per prime. | Caricala: da vicino non fa male a una mosca.
|
||||
|
||||
@@ -1264,6 +1285,7 @@ ed.terrain.ruby = Vena di rubini
|
||||
ed.terrain.diamond = Vena di diamanti
|
||||
ed.terrain.steel = Muro d'acciaio
|
||||
ed.terrain.mushroom = Fungo gigante
|
||||
ed.terrain.grove = Boschetto di funghi
|
||||
ed.terrain.grass = Erba (superficie)
|
||||
ed.terrain.tree = Albero
|
||||
ed.place.core = Nucleo dell'Anima
|
||||
@@ -1346,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
|
||||
@@ -1365,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.
|
||||
@@ -1387,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Ditastorte: Ah! Hai sentito? Qualcosa arriva da est. Preparati: il fu
|
||||
tut.obj6 = Sconfiggi i due cavalieri.
|
||||
tut.knights = Ditastorte: Due cavalieri! Manda le tue creature con la bandiera d'attacco (Ordini) o colpiscili con il fulmine.
|
||||
tut.win = Ditastorte: Ben fatto, padrone! Tutto qui. Be', quasi. Il resto lo racconta la campagna. Metto su il bollitore.
|
||||
|
||||
# master talents
|
||||
cat.master = Maestro
|
||||
talent.title = Talenti del maestro
|
||||
talent.points = Punti talento liberi: {0}
|
||||
talent.next = {0}/{1} esperienza al prossimo punto
|
||||
talent.all = Tutti i talenti sono appresi.
|
||||
talent.builder = Costruttore
|
||||
talent.warlord = Condottiero
|
||||
talent.warlock = Stregone
|
||||
talent.merchant = Mercante
|
||||
talent.builder.desc = Gli Imp scavano il {0}% più in fretta, le stanze costano il {1}% in meno.
|
||||
talent.warlord.desc = Le creature colpiscono il {0}% più forte e subiscono il {1}% di danni in meno.
|
||||
talent.warlock.desc = Il mana scorre il {0}% più in fretta, le magie costano il {1}% in meno.
|
||||
talent.merchant.desc = I salari sono il {0}% più bassi, il tesoro consegnato vale il {1}% in più.
|
||||
talent.none = Non ancora appreso.
|
||||
talent.then = Livello successivo: {0}
|
||||
msg.talent_point = Un nuovo punto talento! Spendilo nella finestra del maestro.
|
||||
msg.talent_learned = {0}: livello {1} appreso.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Sereno
|
||||
weather.rain = Pioggia
|
||||
weather.fog = Nebbia
|
||||
weather.storm = Tempesta
|
||||
msg.weather_clear = In superficie il cielo si rasserena.
|
||||
msg.weather_rain = In superficie inizia a piovere: il fuoco non attecchisce.
|
||||
msg.weather_fog = La nebbia avvolge la terra: all'aperto tutti vedono meno.
|
||||
msg.weather_storm = In superficie si avvicina una tempesta! I fulmini colpiscono unità e alberi.
|
||||
msg.lightning_unit = Un fulmine colpisce {0}!
|
||||
# creature names
|
||||
names.imp = Briciola, Moccolo, Scavino, Fanghetto, Pidocchio, Sudicino, Grattino, Zozzo, Pulcino, Ciottolo, Rospetto, Scricciolo, Mollica, Fuliggine, Tappo, Bricco, Zampetto, Bigolo, Frugolo, Pallino
|
||||
names.kobold = Ringhio, Graffio, Morso, Straccio, Rusco, Zanna, Pizzico, Rognoso, Uncino, Cardo, Spillo, Furbetto, Tarlo, Sgraffa, Ladruncolo, Sdentato, Sassolino, Ruggine, Grinza, Malandrino
|
||||
names.spider = Zampette, Ragnetto, Filuccia, Solletico, Ottopiedi, Setina, Tessitrice, Batuffolo, Streghina, Groviglio, Gomitolo, Aghetto, Trappolina, Zannina, Pois, Merletto, Ricamina, Pungino, Ombretta, Ragnatela
|
||||
names.mage = Bartolomeo, Ignazio, Ermenegildo, Anacleto, Fulgenzio, Eustachio, Leopoldo, Baldassarre, Prospero, Teodoro, Sigismondo, Fiorenzo, Casimiro, Gualtiero, Pancrazio, Aurelio, Ruggero, Cipriano, Serafino, Venceslao
|
||||
names.ogre = Macigno, Testone, Zuccone, Bestione, Mattone, Bruto, Mangione, Ciccione, Ceppo, Tontolone, Tronco, Pancione, Babbeo, Pataccone, Gnocco, Bombo, Ciocco, Sberla, Bamboccione, Somarone
|
||||
names.jailer = Chiavistello, Lucchetto, Ceppi, Cupo, Torvo, Segreta, Sbarra, Bieco, Ferro, Catena, Severo, Paletto, Manetta, Cerbero, Chiavaio, Lugubre, Arcigno, Gattabuia, Spranga, Anello
|
||||
names.priestess = Brace, Favilla, Fiamma, Pira, Scintilla, Cenere, Vermiglia, Falò, Fulgida, Vampa, Morgana, Serafina, Scarlatta, Fiammetta, Ignea, Rubina, Ambra, Fenice, Candela, Lucilla
|
||||
names.chainbreaker = Spezzacatene, Ulfgar, Animaferro, Brannoc, Kragmor, Valdrek, Spino, Gorath, Indomito, Rurik, Torvaldo, Agone, Durnok, Frantumo, Brando, Gualfredo, Korrun, Arvak, Liberatore, Alvardo
|
||||
names.bat = Squittio, Alucce, Squitto, Morsetto, Eco, Pipistrello, Crepuscolo, Zinzino, Cri, Dentino, Svolazzo, Notturno, Falenino, Svolazzino, Picchiata, Pendolino, Vellutino, Ombrino, Strillo, Zigzag
|
||||
names.gargoyle = Granito, Ardesia, Basalto, Selciato, Calcina, Ciottolone, Ghiaia, Selce, Mattone, Maceria, Quarzo, Pomice, Cornicione, Grifone, Musone, Travertino, Frontone, Scalpellino, Scalpello, Macigno
|
||||
names.owl = Saputello, Gufetto, Piuma, Barbagianni, Minerva, Tobia, Civetta, Sapienzio, Piumino, Savio, Meditabondo, Anziano, Assiolo, Noctua, Allocco, Pergamena, Imbrunire, Gufone, Professore, Erudito
|
||||
names.wyrm = Vermitrax, Fauci, Bruciaterra, Calderone, Ignivoro, Fumarolo, Drakkar, Pirro, Ossidiana, Boccafuoco, Vulcanor, Ustione, Malgrath, Tyrax, Inferno, Zolfo, Kaldor, Fyrnak, Ragnor, Tizzone
|
||||
names.cogling = Bulloncino, Ingranaggio, Vitina, Cricchetto, Perno, Pignone, Aggeggio, Arnese, Chiavetta, Molla, Tintinnio, Coso, Rivetto, Ronzio, Manovella, Rondella, Cardine, Marchingegno, Tictac, Molletta
|
||||
names.revenant = Lamento, Pena, Cinereo, Strazio, Vuoto, Lutto, Livido, Dolore, Spettro, Nenia, Tumulo, Penombra, Cripta, Pianto, Veglia, Sudario, Esangue, Elegia, Anima, Desolato
|
||||
names.beast = Arruffo, Criniera, Bruno, Pelone, Sgranocchio, Muschio, Brontolo, Orsetto, Cespuglio, Scrocchio, Guscio, Zampone, Setola, Muffa, Rombo, Rovo, Cricrì, Borbotto, Bestione, Annusino
|
||||
names.human = Rinaldo, Orlando, Corrado, Goffredo, Tristano, Ugo, Galvano, Lancillotto, Astolfo, Tancredi, Bertoldo, Ruggiero, Oliviero, Baldovino, Rambaldo, Guglielmo, Ettore, Gilberto, Rodolfo, Brunetto
|
||||
epithets.mid = Spaccaossa, Pestafango, Mordiratti, Faccialercia, Masticasassi, Picchiateschi, Pugnopuzzo, Nocchedruggine, Sventrapance, Calciastinchi, Barbamuffa, Artigliosozzo, Sparamocci, Mangiaorecchie, Spaccaginocchia, Mangiavermi
|
||||
epithets.high = Regicida, Manofunesta, Zannaterrore, Spaccamondi, Flagelloeroi, Pugnotempesta, Terrornotturno, Mietianime, Cuordiferro, Sfondacastelli, Ammazzadraghi, Passainferi, Signordombra, Coronasangue, Mareafunesta, Spezzastelle
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = De poort brokkelt af! De Poortwachter verschijnt met {0} verdedi
|
||||
msg.gate_defenders = De poort roept {0} verdedigers op!
|
||||
msg.level_up = {0} bereikt niveau {1}.
|
||||
msg.hero_slain = {0} verslagen! Buit: {1} goud.
|
||||
msg.thief_in_treasury = Er zit een dief in je schatkamer!
|
||||
msg.thief_escaped = Een dief is ontsnapt met {0} goud!
|
||||
msg.snare_caught = In een klem getrapt: {0}!
|
||||
msg.converted = De inquisiteur heeft een van je wezens bekeerd: {0}!
|
||||
msg.creature_died = Je {0} is gesneuveld.
|
||||
msg.portal_claimed = Het portaal is van jou! Het trekt wezens aan.
|
||||
msg.flag_removed = Aanvalsvlag verwijderd. Je wezens keren terug.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Niet genoeg mana ({0} nodig).
|
||||
hint.portal_no_lair = Het portaal staat open, maar zonder hol komen er geen wezens.
|
||||
hint.no_farm = Wezens willen eten: bouw een paddenstoeltuin.
|
||||
hint.lair_full = Alle bedden in het hol zijn bezet. Vergroot je hol.
|
||||
hint.creature_max = Je kerker huisvest 100 wezens - er komen er geen meer door het portaal.
|
||||
hint.gate_attack = Je wezens vallen de Heldenpoort aan!
|
||||
hint.treasury_full = Je schatkamers zijn vol! Bouw meer schatkamervakken.
|
||||
hint.flag_unreachable = Je wezens kunnen de aanvalsvlag niet bereiken.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Rechtsklik Unit neerzetten (gevangenen: ook in de martelkame
|
||||
help.k_slap = Shift+Links Hand: eigen units een mep geven, helden op je grond slaan
|
||||
help.k_pan = Rechts slepen Kaart verschuiven (ook WASD/pijltjes, schermrand)
|
||||
help.k_rot = Midden slepen Camera draaien en kantelen (ook Q/E, R/F)
|
||||
help.k_reset = Midden 2x klik Hoek en draaiing terugzetten (plek blijft)
|
||||
help.k_zoom = Muiswiel Zoomen
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Gereedschap kiezen
|
||||
help.k_misc = P / Spatie Pauze M Geluid aan/uit +/- Volume
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Wat erin valt, verbrandt. Stenen bruggen gaan eroverheen.
|
||||
tile.bridge = Houten brug
|
||||
tile.lavabridge = Stenen brug
|
||||
tile.mushroom = Reuzenpaddenstoel
|
||||
tile.mushroom_grown = Volgroeid: klaar om te kappen.
|
||||
tile.mushroom_growing = Groeit: {0}% - pas volgroeid kan hij gekapt worden.
|
||||
tile.grove = Paddenstoelenbosje
|
||||
tile.grove_info = Niemand kan dit mos veroveren. Hier groeien reuzenpaddenstoelen terug.
|
||||
tile.tree = Boom
|
||||
tile.plant_info = Markeer met het houweel: een duiveltje kapt hem voor {0} hout.
|
||||
tile.drawbridge = Ophaalbrug
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Open partijen
|
||||
net.no_rooms = Geen open partijen.
|
||||
net.create = Partij maken
|
||||
net.map_size = Kaart {0}x{1} · {2} spelers
|
||||
net.coop_mission = Co-op-veldtocht: missie {0} ({1})
|
||||
net.coop_create = Co-op-veldtocht
|
||||
net.coop_editor = Editorveldtocht
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = geneest zichzelf met een derde van de schade die hij toebre
|
||||
unit.giant = Reus
|
||||
unit.fairy = Feeënspion
|
||||
unit.battlemage = Strijdmagiër
|
||||
unit.thief = Dief
|
||||
unit.monk = Monnik
|
||||
unit.ranger = Woudloper
|
||||
unit.inquisitor = Inquisiteur
|
||||
unit.ram = Stormram
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = De helden hebben een val vernield: {0}.
|
||||
best.giant.text = Een reus is wat er gebeurt als een ridder driehonderd jaar zijn groente opeet. Traag, enorm, en deuren beschouwt hij als een suggestie. | Hij vernielt vallen gewoon door ernaast te staan. Walgelijke manieren. Raak hem met alles wat je hebt, liefst van ver.
|
||||
best.fairy.text = De kleine verklikker van de Orde: een gloeiende mug met vleugels, vooruitgestuurd om je vallen te vinden en het iedereen te vertellen. Zodra ze een val heeft gezien, slaan de helden hem kapot. | Ze strooit ook geneesstof over de helden. Mep haar als eerste. Twee keer, voor de zekerheid.
|
||||
best.battlemage.text = Een magiër in het gewaad van de Orde die muren als een persoonlijke belediging opvat. Versterkte muren barsten onder zijn vuurballen, en hij blijft er maar over praten. | Breekbaar van dichtbij. Stalen muren ergeren hem het meest - mooi zo.
|
||||
best.thief.text = Vlugge vingers, nog vluggere voeten. Hij komt niet om te vechten: hij sluipt je schatkamer in, vult zijn zak en rent terug naar de poort. | Dood hem voordat hij ontkomt en het goud is weer van jou - met rente.
|
||||
best.monk.text = Jaren van vasten en bidden hebben hem doof gemaakt voor je magie: bliksem, pest en vorst glijden van hem af als regen van een dak. | Vuisten doen ook pijn. Ontvang hem met staal, klauwen en tanden.
|
||||
best.ranger.text = Een woudloper van de Orde die van ver schiet en ijzeren klemmen in je gangen achterlaat. Een wezen dat erin trapt, raakt gewond en zit een tijdje vast. | Klemmen blijven liggen tot iemand erin trapt. Liever een kobold dan je oger.
|
||||
best.inquisitor.text = Broos, maar zijn preken zijn gevaarlijk: elke twintig seconden praat hij een van je zwakste wezens - een kobold of een spin - over naar de kant van de helden. | Sterke wezens lachen hem gewoon uit. Sla als eerste toe.
|
||||
best.ram.text = Een boomstam met ijzeren kop en een dak tegen pijlen. Je wezens laten hem koud – je deuren, muren en torens niet. | Stop hem voor hij de deur bereikt, of zorg voor een nieuwe deur.
|
||||
best.catapult.text = Een houten arm, een emmer stenen en een ridder met te veel vrije tijd. Hij reikt verder dan je torens en mikt eerst op hen. | Storm erop af: van dichtbij doet hij geen vlieg kwaad.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Robijnader
|
||||
ed.terrain.diamond = Diamantader
|
||||
ed.terrain.steel = Stalen muur
|
||||
ed.terrain.mushroom = Reuzenpaddenstoel
|
||||
ed.terrain.grove = Paddenstoelenbosje
|
||||
ed.terrain.grass = Gras (bovengronds)
|
||||
ed.terrain.tree = Boom
|
||||
ed.place.core = Zielenkern
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Krommevinger: Ha! Hoorde je dat? Er komt iets uit het oosten. Maak je
|
||||
tut.obj6 = Versla de twee ridders.
|
||||
tut.knights = Krommevinger: Twee ridders! Stuur je wezens met de aanvalsvlag (Bevelen) of tref ze met de bliksem.
|
||||
tut.win = Krommevinger: Goed gedaan, meester! Meer is het niet. Nou ja, bijna. De veldtocht vertelt de rest. Ik zet water op.
|
||||
|
||||
# master talents
|
||||
cat.master = Meester
|
||||
talent.title = Meestertalenten
|
||||
talent.points = Vrije talentpunten: {0}
|
||||
talent.next = {0}/{1} ervaring tot het volgende punt
|
||||
talent.all = Alle talenten zijn geleerd.
|
||||
talent.builder = Bouwmeester
|
||||
talent.warlord = Krijgsheer
|
||||
talent.warlock = Heksenmeester
|
||||
talent.merchant = Koopman
|
||||
talent.builder.desc = Imps graven {0}% sneller, kamers kosten {1}% minder.
|
||||
talent.warlord.desc = Wezens slaan {0}% harder en krijgen {1}% minder schade.
|
||||
talent.warlock.desc = Mana stroomt {0}% sneller, spreuken kosten {1}% minder.
|
||||
talent.merchant.desc = Lonen zijn {0}% lager, afgeleverde schatten zijn {1}% meer waard.
|
||||
talent.none = Nog niet geleerd.
|
||||
talent.then = Volgend niveau: {0}
|
||||
msg.talent_point = Een nieuw talentpunt! Besteed het in het meestervenster.
|
||||
msg.talent_learned = {0} niveau {1} geleerd.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Helder
|
||||
weather.rain = Regen
|
||||
weather.fog = Mist
|
||||
weather.storm = Storm
|
||||
msg.weather_clear = Bovengronds klaart het op.
|
||||
msg.weather_rain = Bovengronds begint het te regenen - vuur vat geen vlam.
|
||||
msg.weather_fog = Mist trekt over het land - buiten ziet iedereen minder.
|
||||
msg.weather_storm = Bovengronds steekt een storm op! Bliksem treft units en bomen.
|
||||
msg.lightning_unit = Door de bliksem getroffen: {0}!
|
||||
# creature names
|
||||
names.imp = Kruimel, Snotje, Graafje, Modder, Pluis, Kleintje, Wroeter, Smoezel, Peuter, Krabber, Mopper, Brokje, Spartel, Slijkie, Priegel, Knobbel, Schepje, Snuiter, Woelie, Vlekje
|
||||
names.kobold = Kef, Knijp, Grijptand, Schurft, Hap, Roestig, Zwiep, Bijter, Gapper, Rat, Flard, Stekel, Klauw, Stoker, Wortel, Gniffel, Duwer, Kartel, Knik, Schram
|
||||
names.spider = Kriebel, Wolpootje, Spinnetje, Kietel, Netje, Achtpoot, Draadje, Griezeltje, Friemel, Zijdje, Webbie, Hupsel, Pluisbeen, Tekla, Spoeltje, Hapje, Garentje, Plakkie, Wiebel, Naaldje
|
||||
names.mage = Bartholomeus, Ignatius, Leopold, Theobald, Adelbert, Hieronymus, Wendelinus, Godfried, Cornelis, Ottokar, Florentius, Engelbert, Pancratius, Rupertus, Constantijn, Eusebius, Servaas, Barnabas, Archibald, Willibrord
|
||||
names.ogre = Knor, Klomp, Boem, Brok, Bonk, Dommel, Lomp, Klopper, Stamper, Houtkop, Plof, Mops, Bolle, Pens, Knuppel, Os, Rommel, Klont, Bonkie, Gorbo
|
||||
names.jailer = Grim, Grendel, Ketting, Slot, Kerker, IJzerbaard, Boei, Bout, Streng, Tralie, Nurks, Haak, Strop, Sleutel, Duister, Rinkel, Stokman, Cipier, Kooi, Bitter
|
||||
names.priestess = Gloed, Vlam, As, Serafien, Laaie, Vuurtje, Morwen, Brunhilde, Ignia, Sintel, Scharlaken, Vonkje, Raven, Lilith, Tondel, Isolde, Brandje, Nyx, Vesper, Gloria
|
||||
names.chainbreaker = Ketenbreker, Ulfgar, IJzerziel, Brannok, Kragmor, Valdrek, Doorn, Gorath, Ongebonden, Rurik, Torvald, Hagen, Durnok, Splinter, Brand, Wolfert, Korrun, Arvak, Sigbert, Halvard
|
||||
names.bat = Piepje, Fladder, Fiep, Zoef, Echo, Flap, Schemer, Sisje, Tjirp, Tandje, Draai, Vleermuisje, Motje, Piep, Suizer, Bungel, Fluweeltje, Hupsje, Kwiek, Flitsje
|
||||
names.gargoyle = Graniet, Leisteen, Basalt, Grimbert, Mortel, Kiezel, Grind, Vuursteen, Voeg, Puin, Kwarts, Puimsteen, Lijst, Knoest, Steenbaard, Chagrijn, Gevel, Spuier, Beitel, Sokkel
|
||||
names.owl = Oehoe, Wijsneus, Ganzenveer, Bartholdus, Minerva, Tobias, Hoehoe, Uilalia, Veertje, Zucht, Pieker, Oudste, Kauwtje, Noctua, Uilbert, Perkament, Schemer, Bosuil, Professor, Pluimpje
|
||||
names.wyrm = Vermithrax, Asmuil, Schroeier, Calderon, Ignivar, Gloeihoorn, Drakkar, Pyrrhus, Obsidiaan, Gloedmuil, Vulkanor, Brandtand, Malgrath, Tyrax, Inferno, Zwavel, Kaldor, Fafnir, Ragnor, Sinteros
|
||||
names.cogling = Radertje, Tandwiel, Schroefje, Ratel, Bout, Drijfwerk, Prutser, Dinges, Sleuteltje, Rondsel, Rammel, Friemel, Klinknagel, Zoem, Slinger, Draaitol, Flens, Klikklak, Tiktak, Nippel
|
||||
names.revenant = Morgen, Wrok, Asgrauw, Klacht, Hol, Rouw, Bleek, Kommer, Schim, Vergaan, Grafman, Schemering, Grafkelder, Weemoed, Wake, Heuvel, Lijkwade, Elegie, Droesem, Woest
|
||||
names.beast = Ruig, Brombeer, Bruno, Sloddervos, Smikkel, Mosrug, Brommer, Ursa, Knoert, Knaks, Klikker, Kruipert, Borstel, Mos, Gerommel, Braam, Tsjirp, Mopperaar, Lobbes, Snuffel
|
||||
names.human = Gerard, Roeland, Koenraad, Aldert, Tristan, Ewout, Walewein, Lancelot, Ferguut, Hugo, Bertram, Godfried, Ogier, Boudewijn, Reinout, Willem, Diederik, Gijsbrecht, Floris, Dirk
|
||||
epithets.mid = Botbreker, Modderpoot, Rattenbijter, Smoelvuil, Kiezelknager, Schedelklopper, Stinkvuist, Roestknokkel, Buikstomper, Schenenschopper, Schimmelbaard, Vuilklauw, Snotslinger, Orenknager, Knieknakker, Wormvreter
|
||||
epithets.high = Koningsdoder, Onheilsvuist, Schriktand, Wereldbreker, Heldenvloek, Stormvuist, Nachtgruwel, Zielenmaaier, IJzerhart, Burchtbreker, Drakendoder, Hellegaander, Schaduwvorst, Bloedkroon, Grimmetij, Sterrenbreker
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Porten smuldrer! Portvokteren dukker opp med {0} forsvarere!
|
||||
msg.gate_defenders = Porten tilkaller {0} forsvarere!
|
||||
msg.level_up = {0} når nivå {1}.
|
||||
msg.hero_slain = {0} beseiret! Bytte: {1} gull.
|
||||
msg.thief_in_treasury = En tyv er i skattkammeret ditt!
|
||||
msg.thief_escaped = En tyv slapp unna med {0} gull!
|
||||
msg.snare_caught = Tråkket i en saks: {0}!
|
||||
msg.converted = Inkvisitoren har omvendt et av vesenene dine: {0}!
|
||||
msg.creature_died = Din {0} har falt.
|
||||
msg.portal_claimed = Portalen er din! Den tiltrekker skapninger.
|
||||
msg.flag_removed = Angrepsflagget er fjernet. Skapningene dine vender tilbake.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Ikke nok mana ({0} kreves).
|
||||
hint.portal_no_lair = Portalen er åpen, men uten et hi kommer det ingen skapninger.
|
||||
hint.no_farm = Skapninger vil ha mat: bygg en soppehage.
|
||||
hint.lair_full = Alle sengene i hiet er opptatt. Gjør hiet større.
|
||||
hint.creature_max = Fangehullet ditt huser 100 vesener - det kommer ikke flere gjennom portalen.
|
||||
hint.gate_attack = Skapningene dine angriper helteporten!
|
||||
hint.treasury_full = Skattkamrene dine er fulle! Bygg flere skattkammerruter.
|
||||
hint.flag_unreachable = Skapningene dine kan ikke nå angrepsflagget.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Høyreklikk Sett ned en enhet (fanger: også i torturkammeret
|
||||
help.k_slap = Shift+Venstre Hånd: baksveiv egne enheter, slå helter på din grunn
|
||||
help.k_pan = Høyre dra Flytt kartet (også WASD/piler, skjermkant)
|
||||
help.k_rot = Midtre dra Drei og vipp kameraet (også Q/E, R/F)
|
||||
help.k_reset = Midtre 2x klikk Tilbakestill vinkel og dreiing (stedet blir)
|
||||
help.k_zoom = Musehjul Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Velg verktøy
|
||||
help.k_misc = P / Mellomrom Pause M Lyd på/av +/- Volum
|
||||
@@ -258,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
|
||||
@@ -459,6 +467,10 @@ tile.lava_info = Det som faller i, brenner. Steinbroer fører over.
|
||||
tile.bridge = Trebro
|
||||
tile.lavabridge = Steinbro
|
||||
tile.mushroom = Kjempesopp
|
||||
tile.mushroom_grown = Fullvoksen: klar til felling.
|
||||
tile.mushroom_growing = Vokser: {0}% - kan først felles når den er fullvoksen.
|
||||
tile.grove = Soppholt
|
||||
tile.grove_info = Ingen kan erobre denne mosen. Her vokser kjempesopper igjen.
|
||||
tile.tree = Tre
|
||||
tile.plant_info = Merk med hakka: en imp feller den for {0} tre.
|
||||
tile.drawbridge = Vindebro
|
||||
@@ -723,6 +735,7 @@ net.error = {0}
|
||||
net.rooms = Åpne kamper
|
||||
net.no_rooms = Ingen åpne kamper.
|
||||
net.create = Opprett kamp
|
||||
net.map_size = Kart {0}x{1} · {2} spillere
|
||||
net.coop_mission = Co-op-kampanje: oppdrag {0} ({1})
|
||||
net.coop_create = Co-op-kampanje
|
||||
net.coop_editor = Editorkampanje
|
||||
@@ -1082,12 +1095,20 @@ ability.drain.desc = leger seg selv med en tredjedel av skaden den gjør.
|
||||
unit.giant = Kjempe
|
||||
unit.fairy = Feespion
|
||||
unit.battlemage = Kampmagiker
|
||||
unit.thief = Tyv
|
||||
unit.monk = Munk
|
||||
unit.ranger = Jeger
|
||||
unit.inquisitor = Inkvisitor
|
||||
unit.ram = Rambukk
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Heltene ødela en felle: {0}.
|
||||
best.giant.text = En kjempe er det som skjer når en ridder spiser grønnsakene sine i tre hundre år. Treg, enorm, og den ser på dører som et forslag. | Den ødelegger feller bare ved å stå ved siden av dem. Ekle manerer. Treff den med alt, helst på avstand.
|
||||
best.fairy.text = Ordenens lille tyster: en glødende knott med vinger, sendt i forveien for å finne fellene dine og fortelle alle om dem. Når den har sett en felle, knuser heltene den. | Den drysser også legende støv på heltene. Smekk den først. To ganger, for sikkerhets skyld.
|
||||
best.battlemage.text = En magiker i Ordenens kappe som ser på vegger som en personlig fornærmelse. Forsterkede vegger brister under ildkulene hans, og han slutter aldri å snakke om det. | Skjør på nært hold. Stålvegger irriterer ham mest - bra.
|
||||
best.thief.text = Raske fingre, enda raskere føtter. Han kommer ikke for å slåss: han sniker seg inn i skattkammeret ditt, fyller sekken og løper tilbake til porten. | Drep ham før han slipper unna, så er gullet ditt igjen - med renter.
|
||||
best.monk.text = År med faste og bønn har gjort ham døv for magien din: lyn, pest og frost preller av ham som regn av et tak. | Never gjør også vondt. Møt ham med stål, klør og tenner.
|
||||
best.ranger.text = En skogsmann fra Ordenen som skyter på avstand og legger ut jernsakser i gangene dine. Et vesen som tråkker i en, blir skadet og sitter fast en stund. | Saksene blir liggende til noen tråkker i dem. Heller en kobold enn trollet ditt.
|
||||
best.inquisitor.text = Skrøpelig, men prekenene hans er farlige: hvert tjuende sekund snakker han et av de svakeste vesenene dine - en kobold eller en edderkopp - over til heltenes side. | Sterke vesener bare ler av ham. Slå til først.
|
||||
best.ram.text = En trestamme med jernhette og et tak mot piler. Skapningene dine bryr den seg ikke om – dørene, murene og tårnene dine derimot. | Stopp den før den når døra, eller skaff en ny dør.
|
||||
best.catapult.text = En trearm, en bøtte stein og en ridder med for mye fritid. Den rekker lenger enn tårnene dine og sikter på dem først. | Storm fram: på nært hold gjør den ikke en flue fortred.
|
||||
|
||||
@@ -1271,6 +1292,7 @@ ed.terrain.ruby = Rubinåre
|
||||
ed.terrain.diamond = Diamantåre
|
||||
ed.terrain.steel = Stålvegg
|
||||
ed.terrain.mushroom = Kjempesopp
|
||||
ed.terrain.grove = Soppholt
|
||||
ed.terrain.grass = Gress (over jorden)
|
||||
ed.terrain.tree = Tre
|
||||
ed.place.core = Sjelekjerne
|
||||
@@ -1353,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
|
||||
@@ -1372,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.
|
||||
@@ -1394,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
|
||||
@@ -1442,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).
|
||||
@@ -1672,3 +1700,70 @@ tut.slap = Krokfinger: Ha! Hørte du det? Noe kommer fra øst. Gjør deg klar: l
|
||||
tut.obj6 = Beseir de to ridderne.
|
||||
tut.knights = Krokfinger: To riddere! Send skapningene dine med angrepsflagget (Ordrer), eller slå dem med lynet.
|
||||
tut.win = Krokfinger: Godt gjort, mester! Det var det hele. Vel – nesten. Kampanjen forteller resten. Jeg setter på kjelen.
|
||||
|
||||
# master talents
|
||||
cat.master = Mester
|
||||
talent.title = Mestertalenter
|
||||
talent.points = Ledige talentpoeng: {0}
|
||||
talent.next = {0}/{1} erfaring til neste poeng
|
||||
talent.all = Alle talenter er lært.
|
||||
talent.builder = Byggmester
|
||||
talent.warlord = Krigsherre
|
||||
talent.warlock = Heksemester
|
||||
talent.merchant = Kjøpmann
|
||||
talent.builder.desc = Imper graver {0}% raskere, rom koster {1}% mindre.
|
||||
talent.warlord.desc = Skapninger slår {0}% hardere og tar {1}% mindre skade.
|
||||
talent.warlock.desc = Mana flyter {0}% raskere, formler koster {1}% mindre.
|
||||
talent.merchant.desc = Lønningene er {0}% lavere, levert skatt er verdt {1}% mer.
|
||||
talent.none = Ikke lært ennå.
|
||||
talent.then = Neste nivå: {0}
|
||||
msg.talent_point = Et nytt talentpoeng! Bruk det i mestervinduet.
|
||||
msg.talent_learned = {0} nivå {1} lært.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Klart
|
||||
weather.rain = Regn
|
||||
weather.fog = Tåke
|
||||
weather.storm = Storm
|
||||
msg.weather_clear = Over jorden klarner det opp.
|
||||
msg.weather_rain = Over jorden begynner det å regne - ild tar ikke fyr.
|
||||
msg.weather_fog = Tåke driver inn over landet - ute ser alle mindre.
|
||||
msg.weather_storm = Over jorden trekker et uvær opp! Lynet treffer enheter og trær.
|
||||
msg.lightning_unit = Truffet av lynet: {0}!
|
||||
# creature names
|
||||
names.imp = Smule, Snørr, Graver, Søle, Lo, Pjokk, Rotemus, Grisen, Busemann, Kloren, Bøtte, Klatt, Gjørme, Pirk, Knott, Spade, Snute, Pusling, Flekk, Grums
|
||||
names.kobold = Bjeff, Klyp, Gripetann, Skabb, Happ, Rusten, Svupp, Biter, Glefs, Rotta, Fille, Pigg, Klo, Bråk, Rask, Fnis, Dytter, Hakk, Knekk, Skramme
|
||||
names.spider = Krabbe, Ullbein, Kongle, Kile, Nettie, Åttefot, Tråden, Grøssel, Fikle, Silke, Veversken, Hoppsa, Lobein, Tekla, Spole, Napp, Garnnøste, Klister, Vimsa, Nåla
|
||||
names.mage = Bartholomeus, Ignatius, Leopold, Theobald, Adalbert, Hieronymus, Valdemar, Gotfred, Cornelius, Ottokar, Florentin, Engelbrekt, Pankratius, Rupertus, Konstantin, Eusebius, Severin, Barnabas, Archibald, Vitus
|
||||
names.ogre = Brumm, Kloss, Bums, Klump, Dunk, Tomsing, Tufs, Klasker, Tramp, Trehode, Plump, Mops, Bolle, Vom, Klubbe, Okse, Rumle, Klatt, Hump, Gorbo
|
||||
names.jailer = Grim, Slå, Lenke, Lås, Fangehull, Jernskjegg, Bøyle, Bolt, Streng, Sprinkel, Grinete, Krok, Snøre, Nøkkel, Dyster, Klirr, Stokkmann, Vokter, Bur, Bitter
|
||||
names.priestess = Glød, Flamme, Aske, Serafina, Lue, Ildine, Morwen, Brynhild, Ignia, Gnist, Skarlagen, Funke, Ravna, Lilith, Knusk, Isolde, Brannhild, Nyx, Vesper, Glimt
|
||||
names.chainbreaker = Lenkebryter, Ulfgar, Jernsjel, Brannok, Kragmor, Valdrek, Torn, Gorath, Ubundet, Rurik, Torvald, Hagen, Durnok, Splint, Brand, Ulfrik, Korrun, Arvak, Sigurd, Halvard
|
||||
names.bat = Pip, Flagre, Pipa, Susle, Ekko, Flaks, Skumring, Hvisle, Kvitre, Tannsen, Virvel, Flaggermus, Møll, Pippe, Suse, Dingle, Fløyel, Smutt, Hvin, Lynet
|
||||
names.gargoyle = Granitt, Skifer, Basalt, Grimbert, Mørtel, Kisel, Pukk, Flint, Fuge, Steinrøys, Kvarts, Pimpstein, Gesims, Knort, Steinskjegg, Gretten, Gavl, Vannspyer, Meisel, Sokkel
|
||||
names.owl = Hubro, Klokhue, Fjærpenn, Bartholdus, Minerva, Tobias, Huhu, Uglalia, Fjærdrik, Sukk, Grubler, Eldste, Kattugle, Noctua, Uglebert, Pergament, Skumring, Natugle, Professor, Dun
|
||||
names.wyrm = Vermithrax, Askegap, Svie, Calderon, Ignivar, Glødehorn, Drakkar, Pyrrhus, Obsidian, Glødgap, Vulkanor, Brenntann, Malgrath, Tyrax, Inferno, Svovel, Kaldor, Fåvne, Ragnor, Sinderos
|
||||
names.cogling = Tannhjul, Hjulet, Skrue, Skralle, Bolt, Gir, Fikser, Dings, Nøkkelen, Drev, Klirre, Fikle, Nagle, Snurre, Sveiv, Snurrebass, Flens, Klikklakk, Tikktakk, Nippel
|
||||
names.revenant = Morgen, Nag, Askegrå, Klage, Hul, Sorg, Blek, Kummer, Skygge, Mold, Gravmann, Tussmørke, Krypt, Vemod, Våke, Haug, Likklede, Elegi, Berme, Øde
|
||||
names.beast = Lurv, Brummbjørn, Bruno, Raggete, Mumle, Moserygg, Brummer, Ursa, Knort, Knask, Klikker, Kryper, Bust, Mose, Rumling, Tornebusk, Knirk, Gnålepott, Bamse, Snuse
|
||||
names.human = Gerhard, Roland, Konrad, Albrekt, Tristan, Eskil, Gavin, Lancelot, Pål, Håkon, Bertram, Gotfred, Eirik, Sverre, Ragnar, Vilfred, Ditlev, Gilbert, Rasmus, Kjell
|
||||
epithets.mid = Beinknuser, Gjørmefot, Rottebiter, Skittfjes, Steingnager, Skallebanker, Stinkeneve, Rustknoke, Magestøter, Leggsparker, Muggskjegg, Skittklo, Snørrslynge, Øregnager, Kneknekker, Markeeter
|
||||
epithets.high = Kongedreper, Undergangshånd, Redselstann, Verdensknuser, Heltebane, Stormneve, Nattskrekk, Sjelehøster, Jernhjerte, Borgknuser, Drageskrekk, Helvetesvandrer, Skyggefyrste, Blodkrone, Dommeflod, Stjerneknuser
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = O portão está a ruir! Surge o Guardião do Portão com {0} def
|
||||
msg.gate_defenders = O portão invoca {0} defensores!
|
||||
msg.level_up = {0} atinge o nível {1}.
|
||||
msg.hero_slain = {0} derrotado! Saque: {1} de ouro.
|
||||
msg.thief_in_treasury = Há um ladrão na tua tesouraria!
|
||||
msg.thief_escaped = Um ladrão fugiu com {0} de ouro!
|
||||
msg.snare_caught = Caiu numa armadilha: {0}!
|
||||
msg.converted = O inquisidor converteu uma das tuas criaturas: {0}!
|
||||
msg.creature_died = O teu {0} caiu.
|
||||
msg.portal_claimed = O portal é teu! Atrai criaturas.
|
||||
msg.flag_removed = Bandeira de ataque retirada. As tuas criaturas regressam.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Mana insuficiente ({0} necessária).
|
||||
hint.portal_no_lair = O portal está aberto, mas sem covil não vem nenhuma criatura.
|
||||
hint.no_farm = As criaturas querem comer: constrói uma horta de cogumelos.
|
||||
hint.lair_full = Todas as camas do covil estão ocupadas. Aumenta o covil.
|
||||
hint.creature_max = A tua masmorra abriga 100 criaturas - não vêm mais pelo portal.
|
||||
hint.gate_attack = As tuas criaturas estão a atacar o Portão dos Heróis!
|
||||
hint.treasury_full = As tuas tesourarias estão cheias! Constrói mais casas de tesouraria.
|
||||
hint.flag_unreachable = As tuas criaturas não conseguem chegar à bandeira de ataque.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Clique dir. Largar unidade (prisioneiros: também na câmara
|
||||
help.k_slap = Shift+Esq. Mão: esbofetear as tuas unidades, socar heróis no teu chão
|
||||
help.k_pan = Arrastar dir. Mover o mapa (também WASD/setas, borda do ecrã)
|
||||
help.k_rot = Arrastar meio Rodar e inclinar a câmara (também Q/E, R/F)
|
||||
help.k_reset = Meio 2x clique Repor ângulo e rotação (o local mantém-se)
|
||||
help.k_zoom = Roda do rato Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Escolher ferramenta
|
||||
help.k_misc = P / Espaço Pausa M Som lig./desl. +/- Volume
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = O que cai lá dentro arde. As pontes de pedra atravessam-na.
|
||||
tile.bridge = Ponte de madeira
|
||||
tile.lavabridge = Ponte de pedra
|
||||
tile.mushroom = Cogumelo gigante
|
||||
tile.mushroom_grown = Crescido: pronto para cortar.
|
||||
tile.mushroom_growing = A crescer: {0}% - só pode ser cortado quando estiver crescido.
|
||||
tile.grove = Bosque de cogumelos
|
||||
tile.grove_info = Ninguém pode conquistar este musgo. Aqui voltam a crescer cogumelos gigantes.
|
||||
tile.tree = Árvore
|
||||
tile.plant_info = Marca-o com a picareta: um diabrete abate-o por {0} de madeira.
|
||||
tile.drawbridge = Ponte levadiça
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Partidas abertas
|
||||
net.no_rooms = Não há partidas abertas.
|
||||
net.create = Criar partida
|
||||
net.map_size = Mapa {0}x{1} · {2} jogadores
|
||||
net.coop_mission = Campanha cooperativa: missão {0} ({1})
|
||||
net.coop_create = Campanha coop.
|
||||
net.coop_editor = Campanha do editor
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = cura-se num terço do dano que causa.
|
||||
unit.giant = Gigante
|
||||
unit.fairy = Fada Espiã
|
||||
unit.battlemage = Mago de Batalha
|
||||
unit.thief = Ladrão
|
||||
unit.monk = Monge
|
||||
unit.ranger = Batedor
|
||||
unit.inquisitor = Inquisidor
|
||||
unit.ram = Aríete
|
||||
unit.catapult = Catapulta
|
||||
msg.trap_wrecked = Os heróis destruíram uma armadilha: {0}.
|
||||
best.giant.text = Um gigante é o que acontece quando um cavaleiro come os legumes durante trezentos anos. Lento, enorme, e trata as portas como uma sugestão. | Destrói armadilhas só por estar ao lado delas. Maneiras repugnantes. Atira-lhe com tudo, de preferência de longe.
|
||||
best.fairy.text = A pequena informadora da Ordem: um mosquito luminoso com asas, mandado à frente para encontrar as tuas armadilhas e contar a toda a gente. Depois de ver uma armadilha, os heróis destroem-na. | Também polvilha pó curativo sobre os heróis. Esmaga-a primeiro. Duas vezes, para ter a certeza.
|
||||
best.battlemage.text = Um mago com as vestes da Ordem que acha as paredes um insulto pessoal. As paredes reforçadas rebentam com as bolas de fogo dele, e ele nunca se cala sobre isso. | Frágil de perto. As paredes de aço irritam-no mais do que tudo - ótimo.
|
||||
best.thief.text = Dedos ligeiros, pés ainda mais ligeiros. Não vem para lutar: esgueira-se para a tua tesouraria, enche o saco e corre de volta para o portão. | Mata-o antes que fuja e o ouro volta a ser teu - com juros.
|
||||
best.monk.text = Anos de jejum e oração tornaram-no surdo à tua magia: relâmpago, praga e gelo escorrem-lhe como chuva num telhado. | Os punhos também doem. Recebe-o com aço, garras e dentes.
|
||||
best.ranger.text = Um homem dos bosques da Ordem que dispara de longe e deixa armadilhas de ferro nos teus corredores. A criatura que pisa uma fica ferida e presa durante um bocado. | As armadilhas ficam até alguém as pisar. Antes um kobold do que o teu ogre.
|
||||
best.inquisitor.text = Frágil, mas os seus sermões são perigosos: a cada vinte segundos convence uma das tuas criaturas mais fracas - um kobold ou uma aranha - a juntar-se aos heróis. | As criaturas fortes riem-se dele. Ataca primeiro.
|
||||
best.ram.text = Um tronco com ponta de ferro e um telhado contra as flechas. As tuas criaturas não lhe interessam – só as tuas portas, muros e torres. | Trava-o antes de chegar à porta, ou arranja outra porta.
|
||||
best.catapult.text = Um braço de madeira, um balde de pedras e um cavaleiro com tempo a mais. Alcança mais longe que as tuas torres e aponta-lhes primeiro. | Carrega sobre ela: de perto não faz mal a uma mosca.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Veio de rubis
|
||||
ed.terrain.diamond = Veio diamantes
|
||||
ed.terrain.steel = Parede de aço
|
||||
ed.terrain.mushroom = Cogumelo gigante
|
||||
ed.terrain.grove = Bosque de cogumelos
|
||||
ed.terrain.grass = Relva (superfície)
|
||||
ed.terrain.tree = Árvore
|
||||
ed.place.core = Núcleo de alma
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Dedotorto: Ah! Ouviste aquilo? Vem aí qualquer coisa do leste. Prepa
|
||||
tut.obj6 = Derrota os dois cavaleiros.
|
||||
tut.knights = Dedotorto: Dois cavaleiros! Envia as tuas criaturas com a bandeira de ataque (Ordens) ou atinge-os com o relâmpago.
|
||||
tut.win = Dedotorto: Muito bem, mestre! É tudo. Bem, quase. A campanha conta o resto. Vou pôr a chaleira ao lume.
|
||||
|
||||
# master talents
|
||||
cat.master = Mestre
|
||||
talent.title = Talentos do mestre
|
||||
talent.points = Pontos de talento livres: {0}
|
||||
talent.next = {0}/{1} de experiência até ao próximo ponto
|
||||
talent.all = Todos os talentos foram aprendidos.
|
||||
talent.builder = Construtor
|
||||
talent.warlord = Senhor da Guerra
|
||||
talent.warlock = Bruxo
|
||||
talent.merchant = Mercador
|
||||
talent.builder.desc = Os diabretes cavam {0}% mais depressa e as salas custam {1}% menos.
|
||||
talent.warlord.desc = As criaturas batem {0}% mais forte e sofrem {1}% menos dano.
|
||||
talent.warlock.desc = A mana flui {0}% mais depressa e os feitiços custam {1}% menos.
|
||||
talent.merchant.desc = Os salários são {0}% mais baixos e o tesouro entregue vale {1}% mais.
|
||||
talent.none = Ainda não aprendido.
|
||||
talent.then = Próximo nível: {0}
|
||||
msg.talent_point = Um novo ponto de talento! Gasta-o na janela do mestre.
|
||||
msg.talent_learned = {0}: nível {1} aprendido.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Limpo
|
||||
weather.rain = Chuva
|
||||
weather.fog = Nevoeiro
|
||||
weather.storm = Temporal
|
||||
msg.weather_clear = O céu limpa à superfície.
|
||||
msg.weather_rain = Começa a chover à superfície - o fogo não pega.
|
||||
msg.weather_fog = O nevoeiro cobre a terra - lá fora todos veem menos.
|
||||
msg.weather_storm = Aproxima-se uma tempestade à superfície! Os raios atingem unidades e árvores.
|
||||
msg.lightning_unit = Um raio atinge {0}!
|
||||
# creature names
|
||||
names.imp = Ranhoso, Migalha, Cavuco, Lamacento, Piolho, Sujinho, Coçadinho, Porcalhão, Pintinho, Seixinho, Sapinho, Fuligem, Rolha, Tampinha, Patinha, Fuçador, Bolinha, Remelas, Grudinho, Tiquinho
|
||||
names.kobold = Rosnado, Arranhão, Mordida, Trapo, Ranzinza, Presa, Beliscão, Sarnento, Gancho, Cardo, Alfinete, Espertalhão, Caruncho, Unhaca, Gatuno, Banguela, Pedrinha, Ferrugem, Ruga, Malandro
|
||||
names.spider = Perninhas, Aranhinha, Fiozinho, Cosquinha, Oitopés, Sedinha, Tecedeira, Fofura, Bruxinha, Embaraço, Novelo, Agulhinha, Armadilha, Dentinha, Bolinhas, Rendinha, Bordadeira, Picadinha, Sombrinha, Teiazinha
|
||||
names.mage = Bartolomeu, Inácio, Hermenegildo, Anacleto, Fulgêncio, Eustáquio, Leopoldo, Baltasar, Próspero, Teodoro, Segismundo, Florêncio, Casimiro, Gualtério, Pancrácio, Aurélio, Rogério, Cipriano, Serafim, Venceslau
|
||||
names.ogre = Pedregulho, Cabeção, Toco, Brutamontes, Tijolo, Bruto, Comilão, Barrigudo, Cepo, Tontão, Tronco, Pançudo, Pateta, Bolão, Nhoque, Bumbo, Torrão, Tabefe, Mamute, Jumento
|
||||
names.jailer = Ferrolho, Cadeado, Grilhão, Sombrio, Torvo, Masmorra, Grade, Carrancudo, Ferro, Corrente, Severo, Tranca, Algema, Cérbero, Chaveiro, Lúgubre, Rabugento, Calabouço, Trinco, Argola
|
||||
names.priestess = Brasa, Fagulha, Chama, Pira, Centelha, Cinza, Rubra, Fogueira, Fulgor, Labareda, Morgana, Serafina, Escarlate, Chispa, Ígnea, Rubi, Âmbar, Fênix, Candeia, Lumina
|
||||
names.chainbreaker = Quebragrilho, Ulfgar, Almaferro, Brannoc, Kragmor, Valdrek, Espinho, Gorath, Indomável, Rurik, Torvaldo, Hagan, Durnok, Estilhaço, Brando, Wulfrico, Korrun, Arvak, Libertador, Halvardo
|
||||
names.bat = Guincho, Asinhas, Pio, Mordisco, Eco, Morceguito, Crepúsculo, Zunzum, Chiadinho, Dentinho, Revoada, Noturno, Mariposa, Bicudinho, Rasante, Penduricalho, Veludo, Sombrio, Grito, Ziguezague
|
||||
names.gargoyle = Granito, Ardósia, Basalto, Pedrão, Argamassa, Calhau, Cascalho, Pederneira, Tijolão, Entulho, Quartzo, Pomes, Cornija, Grifo, Carrancão, Cantaria, Frontão, Canteiro, Cinzel, Penedo
|
||||
names.owl = Sabichão, Corujinha, Pena, Mocho, Minerva, Tobias, Coruja, Sapiêncio, Penugem, Sábio, Pensativo, Ancião, Caburé, Noctua, Bufo, Pergaminho, Ocaso, Murucututu, Professor, Erudito
|
||||
names.wyrm = Vermitrax, Bocarra, Chamusco, Caldeirão, Ignívoro, Fumarola, Drakkar, Pirro, Obsidiana, Boquifogo, Vulcanor, Abrasador, Malgrath, Tyrax, Inferno, Enxofre, Kaldor, Fyrnak, Ragnor, Tição
|
||||
names.cogling = Parafusinho, Engrenagem, Porquinha, Catraca, Pino, Pinhão, Geringonça, Treco, Chavinha, Mola, Tlintlim, Bugiganga, Rebite, Zumbido, Manivela, Arruela, Dobradiça, Engenhoca, Tiquetaque, Molinha
|
||||
names.revenant = Lamento, Pesar, Cinzento, Quebranto, Vazio, Luto, Lívido, Pena, Espectro, Nênia, Túmulo, Penumbra, Cripta, Pranto, Vigília, Mortalha, Sudário, Elegia, Alma, Desolado
|
||||
names.beast = Desgrenhado, Jubão, Bruno, Peludão, Mastigão, Musgoso, Resmungão, Ursinho, Moita, Crocante, Casca, Patudo, Cerdas, Mofo, Ronco, Espinheiro, Estalido, Rezingão, Mamutão, Fungador
|
||||
names.human = Rodrigo, Gonçalo, Álvaro, Fernão, Tristão, Nuno, Galvão, Lançarote, Pelágio, Henrique, Beltrão, Godofredo, Afonso, Balduíno, Ramiro, Martim, Egas, Gilberto, Vasco, Duarte
|
||||
epithets.mid = Quebra-Ossos, Pé-de-Lama, Morde-Ratos, Cara-Suja, Rói-Pedras, Racha-Crânios, Punho-Fedido, Nó-Ferrugem, Arromba-Panças, Chuta-Canelas, Barba-Mofo, Garra-Suja, Atira-Ranho, Masca-Orelhas, Parte-Joelhos, Come-Minhocas
|
||||
epithets.high = Mata-Reis, Mão-Funesta, Presa-Terror, Quebra-Mundos, Flagelo-Heróis, Punho-Tormenta, Terror-Noturno, Ceifa-Almas, Coração-Ferro, Derruba-Castelos, Mata-Dragões, Anda-Infernos, Senhor-Sombra, Coroa-Sangue, Maré-Funesta, Parte-Estrelas
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Poarta se năruie! Paznicul Porții apare cu {0} apărători!
|
||||
msg.gate_defenders = Poarta cheamă {0} apărători!
|
||||
msg.level_up = {0} ajunge la nivelul {1}.
|
||||
msg.hero_slain = {0} învins! Pradă: {1} aur.
|
||||
msg.thief_in_treasury = Un hoț e în vistieria ta!
|
||||
msg.thief_escaped = Un hoț a scăpat cu {0} aur!
|
||||
msg.snare_caught = A călcat într-o capcană: {0}!
|
||||
msg.converted = Inchizitorul ți-a convertit una dintre creaturi: {0}!
|
||||
msg.creature_died = {0} al tău a căzut.
|
||||
msg.portal_claimed = Portalul e al tău! Atrage creaturi.
|
||||
msg.flag_removed = Steagul de atac a fost scos. Creaturile tale se întorc.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Nu ai destulă mana (necesar {0}).
|
||||
hint.portal_no_lair = Portalul e deschis, dar fără bârlog nu vine nicio creatură.
|
||||
hint.no_farm = Creaturile vor să mănânce: construiește o grădină de ciuperci.
|
||||
hint.lair_full = Toate paturile din bârlog sunt ocupate. Mărește-ți bârlogul.
|
||||
hint.creature_max = Temnița ta adăpostește 100 de creaturi - nu mai vin altele prin portal.
|
||||
hint.gate_attack = Creaturile tale atacă Poarta Eroilor!
|
||||
hint.treasury_full = Vistieriile tale sunt pline! Construiește mai multe dale de vistierie.
|
||||
hint.flag_unreachable = Creaturile tale nu pot ajunge la steagul de atac.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Clic dreapta Lasă jos o unitate (prizonieri: și în camera d
|
||||
help.k_slap = Shift+Stânga Mână: palmă unităților tale, pumn eroilor de pe terenul tău
|
||||
help.k_pan = Dreapta+trage Mută harta (și WASD/săgeți, marginea ecranului)
|
||||
help.k_rot = Mijloc+trage Rotește și înclină camera (și Q/E, R/F)
|
||||
help.k_reset = Mijloc 2x clic Resetează unghiul și rotația (locul rămâne)
|
||||
help.k_zoom = Rotița mouse Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Alege unealta
|
||||
help.k_misc = P / Spațiu Pauză M Sunet pornit/oprit +/- Volum
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Ce cade înăuntru arde. Podurile de piatră o traversează.
|
||||
tile.bridge = Pod de lemn
|
||||
tile.lavabridge = Pod de piatră
|
||||
tile.mushroom = Ciupercă uriașă
|
||||
tile.mushroom_grown = Crescută: gata de tăiat.
|
||||
tile.mushroom_growing = Crește: {0}% - poate fi tăiată doar când e crescută.
|
||||
tile.grove = Crâng de ciuperci
|
||||
tile.grove_info = Nimeni nu poate cuceri acest mușchi. Aici cresc din nou ciuperci uriașe.
|
||||
tile.tree = Copac
|
||||
tile.plant_info = Marchează-l cu târnăcopul: un drăcușor îl doboară pentru {0} lemn.
|
||||
tile.drawbridge = Pod mobil
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Meciuri deschise
|
||||
net.no_rooms = Niciun meci deschis.
|
||||
net.create = Creează meci
|
||||
net.map_size = Hartă {0}x{1} · {2} jucători
|
||||
net.coop_mission = Campanie co-op: misiunea {0} ({1})
|
||||
net.coop_create = Campanie co-op
|
||||
net.coop_editor = Campanie din editor
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = se vindecă cu o treime din daunele pe care le provoacă.
|
||||
unit.giant = Uriaș
|
||||
unit.fairy = Zână Iscoadă
|
||||
unit.battlemage = Mag de Luptă
|
||||
unit.thief = Hoț
|
||||
unit.monk = Călugăr
|
||||
unit.ranger = Pândar
|
||||
unit.inquisitor = Inchizitor
|
||||
unit.ram = Berbec
|
||||
unit.catapult = Catapultă
|
||||
msg.trap_wrecked = Eroii au distrus o capcană: {0}.
|
||||
best.giant.text = Un uriaș e ce se întâmplă când un cavaler își mănâncă legumele trei sute de ani. Lent, enorm, iar ușile le tratează drept sugestii. | Distruge capcanele doar stând lângă ele. Maniere respingătoare. Lovește-l cu tot ce ai, de preferință de departe.
|
||||
best.fairy.text = Micul informator al Ordinului: un țânțar strălucitor cu aripi, trimis înainte să-ți găsească capcanele și să le spună tuturor. Odată ce a văzut o capcană, eroii o sparg. | Mai presară și praf vindecător peste eroi. Plesnește-o prima. De două ori, ca să fii sigur.
|
||||
best.battlemage.text = Un mag în veșmintele Ordinului care consideră zidurile o insultă personală. Zidurile întărite crapă sub mingile lui de foc și nu se mai oprește din vorbit despre asta. | Fragil de aproape. Zidurile de oțel îl enervează cel mai tare - bine.
|
||||
best.thief.text = Degete iuți, picioare și mai iuți. Nu vine să lupte: se strecoară în vistieria ta, își umple sacul și fuge înapoi la poartă. | Omoară-l înainte să scape și aurul e din nou al tău - cu dobândă.
|
||||
best.monk.text = Ani de post și rugăciune l-au făcut surd la magia ta: fulgerul, ciuma și gerul alunecă de pe el ca ploaia de pe acoperiș. | Și pumnii dor. Întâmpină-l cu oțel, gheare și dinți.
|
||||
best.ranger.text = Un om al pădurii din Ordin care trage de departe și lasă capcane de fier pe coridoarele tale. Creatura care calcă într-una e rănită și rămâne prinsă o vreme. | Capcanele rămân până calcă cineva în ele. Mai bine un kobold decât căpcăunul tău.
|
||||
best.inquisitor.text = Firav, dar predicile lui sunt periculoase: la fiecare douăzeci de secunde convinge una dintre cele mai slabe creaturi ale tale - un kobold sau un păianjen - să treacă de partea eroilor. | Creaturile puternice doar râd de el. Lovește primul.
|
||||
best.ram.text = Un trunchi cu vârf de fier și un acoperiș contra săgeților. Creaturile tale nu-l interesează – ușile, zidurile și turnurile da. | Oprește-l înainte de ușă sau fă-ți rost de alta.
|
||||
best.catapult.text = Un braț de lemn, o găleată de pietre și un cavaler cu prea mult timp liber. Bate mai departe decât turnurile tale și le țintește primele. | Năvălește peste ea: de aproape nu face rău nici unei muște.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Filon de rubine
|
||||
ed.terrain.diamond = Filon de diamante
|
||||
ed.terrain.steel = Zid de oțel
|
||||
ed.terrain.mushroom = Ciupercă uriașă
|
||||
ed.terrain.grove = Crâng de ciuperci
|
||||
ed.terrain.grass = Iarbă (suprafață)
|
||||
ed.terrain.tree = Copac
|
||||
ed.place.core = Miezul sufletului
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Crookfinger: Ha! Ai auzit? Ceva vine dinspre est. Pregătește-te: fu
|
||||
tut.obj6 = Învinge cei doi cavaleri.
|
||||
tut.knights = Crookfinger: Doi cavaleri! Trimite-ți creaturile cu steagul de atac (Ordine) sau lovește-i cu fulgerul.
|
||||
tut.win = Crookfinger: Bravo, stăpâne! Asta e tot. Ei - aproape. Campania îți spune restul. Pun de un ceai.
|
||||
|
||||
# master talents
|
||||
cat.master = Stăpân
|
||||
talent.title = Talentele stăpânului
|
||||
talent.points = Puncte de talent libere: {0}
|
||||
talent.next = {0}/{1} experiență până la următorul punct
|
||||
talent.all = Toate talentele sunt învățate.
|
||||
talent.builder = Constructor
|
||||
talent.warlord = Căpetenie
|
||||
talent.warlock = Vrăjitor
|
||||
talent.merchant = Negustor
|
||||
talent.builder.desc = Drăcușorii sapă cu {0}% mai repede, camerele costă cu {1}% mai puțin.
|
||||
talent.warlord.desc = Creaturile lovesc cu {0}% mai tare și primesc cu {1}% mai puține daune.
|
||||
talent.warlock.desc = Mana curge cu {0}% mai repede, vrăjile costă cu {1}% mai puțin.
|
||||
talent.merchant.desc = Simbriile sunt cu {0}% mai mici, comorile livrate valorează cu {1}% mai mult.
|
||||
talent.none = Încă neînvățat.
|
||||
talent.then = Nivelul următor: {0}
|
||||
msg.talent_point = Un nou punct de talent! Folosește-l în fereastra stăpânului.
|
||||
msg.talent_learned = {0}: nivelul {1} învățat.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Senin
|
||||
weather.rain = Ploaie
|
||||
weather.fog = Ceață
|
||||
weather.storm = Furtună
|
||||
msg.weather_clear = La suprafață cerul se înseninează.
|
||||
msg.weather_rain = La suprafață începe să plouă - focul nu se aprinde.
|
||||
msg.weather_fog = Ceața se lasă peste ținut - afară toată lumea vede mai puțin.
|
||||
msg.weather_storm = La suprafață se pregătește o furtună! Fulgerele lovesc unități și copaci.
|
||||
msg.lightning_unit = Fulgerul a lovit: {0}!
|
||||
# creature names
|
||||
names.imp = Firimiță, Mucos, Săpăcel, Noroiaș, Scamă, Pițigoi, Scormon, Mânjitu, Murdărel, Zgârie, Bulgăraș, Pată, Glodaș, Pârlitu, Cocoloș, Lopățică, Râmățel, Mărunțel, Pătuță, Cârtiță
|
||||
names.kobold = Lătrău, Ciupitu, Colțan, Râios, Mușcătoru, Rugină, Șmecheru, Rozătoru, Căscatu, Șobolan, Zdrențe, Spin, Gheară, Cârcotaș, Rădăcină, Chicotitu, Îmbrâncitu, Știrbu, Pârâitu, Zgârietură
|
||||
names.spider = Picioruș, Lânos, Păiănjel, Gâdilici, Plăsuță, Optpicioare, Firicel, Fiorică, Bâjbâilă, Mătăsica, Țesătoarea, Săltăreț, Pufoșel, Tecla, Mosorel, Prinzătoru, Ghemuleț, Lipicioasa, Sfârlează, Acușor
|
||||
names.mage = Bartolomeu, Ignațiu, Leopold, Teobald, Adalbert, Ieronim, Valdemar, Gotfrid, Corneliu, Otocar, Florentin, Engelbert, Pancrațiu, Ruprecht, Constantin, Eusebiu, Severin, Barnaba, Archibald, Bonifaciu
|
||||
names.ogre = Bolovan, Butuc, Huidumă, Stâncă, Nătăfleț, Mătăhală, Bufnitu, Ciomag, Tropăitu, Căpățână, Pleosc, Burtosu, Ciolan, Pântecosu, Măciucă, Bou, Bubuitu, Hălcos, Toba, Gorbo
|
||||
names.jailer = Posac, Zăvor, Lanț, Lacăt, Temniță, Barbăfier, Belciug, Șurub, Aspru, Gratie, Morocănos, Cârlig, Ștreang, Chelar, Întunecatu, Zornăitu, Butucar, Paznic, Cușcă, Amar
|
||||
names.priestess = Jăratic, Flacăra, Cenușa, Serafina, Văpaia, Focoasa, Morgana, Brunhilda, Ignia, Scânteia, Purpura, Jarița, Corbița, Lilith, Iasca, Isolda, Pârjolia, Nyx, Vesperia, Flăcăruia
|
||||
names.chainbreaker = Rupelanțuri, Ulfgar, Sufletfier, Brannoc, Kragmor, Valdrek, Spinu, Gorath, Neînfrânt, Rurik, Torvald, Hagen, Durnok, Țandără, Pârjol, Luposlav, Korrun, Arvak, Eliberatoru, Halvard
|
||||
names.bat = Chițăilă, Fâlfâitu, Piu, Foșnet, Ecou, Fluturel, Amurg, Fluierici, Țârâitu, Colțișor, Vârtej, Liliecel, Molia, Piuitu, Vâjâitu, Atârnatu, Catifea, Strecuratu, Țipătu, Fulger
|
||||
names.gargoyle = Granit, Ardezie, Bazalt, Chipdepiatră, Mortar, Pietricel, Prundiș, Cremene, Rost, Dărâmătură, Cuarț, Ponce, Cornișă, Nod, Pietroi, Bosumflatu, Fronton, Burlan, Dăltiță, Soclu
|
||||
names.owl = Buhă, Deșteptu, Pană, Bartold, Minerva, Tobias, Uhu, Bufnițica, Penuța, Oftat, Gânditoru, Bătrânu, Cucuvea, Noctua, Huhurez, Pergament, Amurgel, Ciuf, Profesoru, Puf
|
||||
names.wyrm = Vermithrax, Gurăcenușă, Pârjolitu, Calderon, Ignivar, Cornjar, Drakkar, Pyrrhus, Obsidian, Gurădefoc, Vulcanor, Colțjar, Malgrath, Tyrax, Infernu, Pucioasă, Kaldor, Fafnir, Ragnor, Tăciune
|
||||
names.cogling = Rotiță, Angrenaj, Șurubel, Clichet, Nituț, Pinion, Meșteraș, Drăcovenie, Cheiță, Arcuș, Clinchet, Butonaș, Piuliță, Bâzâitu, Manivelă, Titirez, Flanșă, Clic, Tictac, Arculeț
|
||||
names.revenant = Jale, Amărăciune, Cenușiu, Geamăt, Gol, Doliu, Palid, Durere, Umbră, Țărână, Gropar, Asfințit, Criptă, Dor, Priveghi, Movilă, Giulgiu, Elegie, Drojdie, Pustiu
|
||||
names.beast = Zbârlitu, Moșu, Bruno, Ciufulitu, Plescăitu, Mușchiosu, Mormăitu, Ursuleț, Tufiș, Ronțăitu, Țăcănitu, Târâtu, Țepos, Mușchi, Bubuit, Mur, Cârâitu, Bombănitu, Martin, Adulmecătoru
|
||||
names.human = Gerard, Roland, Conrad, Albert, Tristan, Eckhart, Gawain, Lancelot, Parsifal, Hugo, Bertram, Godefroy, Ortwin, Balduin, Reinhold, Wilfrid, Ditrich, Gilbert, Ruprecht, Radu
|
||||
epithets.mid = Sparge-Oase, Picior-de-Noroi, Roade-Șobolani, Mutră-Murdară, Mestecă-Pietre, Sparge-Țeste, Pumn-Puturos, Os-Ruginit, Spintecă-Burți, Bate-Fluiere, Barbă-Mucegăită, Gheară-Murdară, Aruncă-Muci, Roade-Urechi, Rupe-Genunchi, Mănâncă-Viermi
|
||||
epithets.high = Omoară-Regi, Mână-Blestemată, Colț-de-Groază, Sfarmă-Lumi, Pierzania-Eroilor, Pumn-de-Furtună, Groaza-Nopții, Secerător-de-Suflete, Inimă-de-Fier, Dărâmă-Cetăți, Omoară-Dragoni, Umblă-prin-Iad, Stăpânul-Umbrelor, Coroană-de-Sânge, Val-de-Osândă, Sfarmă-Stele
|
||||
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ă
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Врата рушатся! Появляется Страж вр
|
||||
msg.gate_defenders = Врата призывают {0} защитников!
|
||||
msg.level_up = {0}: достигнут уровень {1}.
|
||||
msg.hero_slain = Повержен: {0}! Добыча: {1} золота.
|
||||
msg.thief_in_treasury = Вор в твоей сокровищнице!
|
||||
msg.thief_escaped = Вор сбежал с {0} золота!
|
||||
msg.snare_caught = Попал в капкан: {0}!
|
||||
msg.converted = Инквизитор обратил одно из твоих существ: {0}!
|
||||
msg.creature_died = Пал твой боец: {0}.
|
||||
msg.portal_claimed = Портал твой! Он привлекает существ.
|
||||
msg.flag_removed = Флаг атаки убран. Существа возвращаются.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Не хватает маны (нужно {0}).
|
||||
hint.portal_no_lair = Портал открыт, но без логова существа не придут.
|
||||
hint.no_farm = Существа хотят есть: построй грибной сад.
|
||||
hint.lair_full = Все места в логове заняты. Расширь логово.
|
||||
hint.creature_max = В твоём подземелье 100 существ - больше через портал не придёт.
|
||||
hint.gate_attack = Твои существа атакуют Врата героев!
|
||||
hint.treasury_full = Сокровищницы полны! Построй больше клеток сокровищницы.
|
||||
hint.flag_unreachable = Существа не могут добраться до флага атаки.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Правый клик Опустить существо (пл
|
||||
help.k_slap = Shift+Левый Рука: шлепок своим, удар героям на своей земле
|
||||
help.k_pan = Тянуть правой Двигать карту (также WASD/стрелки, край экрана)
|
||||
help.k_rot = Тянуть средней Вращать и наклонять камеру (также Q/E, R/F)
|
||||
help.k_reset = Средняя 2x щелчок Сбросить угол и поворот (место остаётся)
|
||||
help.k_zoom = Колесо мыши Масштаб
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Выбор инструмента
|
||||
help.k_misc = P / Пробел Пауза M Звук вкл/выкл +/- Громкость
|
||||
@@ -258,6 +264,8 @@ val.medium = Средне
|
||||
val.high = Высоко
|
||||
val.normal = Обычный
|
||||
val.large = Большой
|
||||
val.small = Маленький
|
||||
val.auto_plain = Авто
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Камера вперёд
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Всё, что туда падает, сгорает. Её пе
|
||||
tile.bridge = Деревянный мост
|
||||
tile.lavabridge = Каменный мост
|
||||
tile.mushroom = Гигантский гриб
|
||||
tile.mushroom_grown = Вырос: можно рубить.
|
||||
tile.mushroom_growing = Растёт: {0}% - срубить можно только взрослый гриб.
|
||||
tile.grove = Грибная роща
|
||||
tile.grove_info = Этот мох нельзя захватить. Здесь снова вырастают гигантские грибы.
|
||||
tile.tree = Дерево
|
||||
tile.plant_info = Отметьте киркой: бес срубит его за {0} дерева.
|
||||
tile.drawbridge = Подъёмный мост
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Открытые матчи
|
||||
net.no_rooms = Нет открытых матчей.
|
||||
net.create = Создать матч
|
||||
net.map_size = Карта {0}x{1} · {2} игроков
|
||||
net.coop_mission = Кооп-кампания: миссия {0} ({1})
|
||||
net.coop_create = Кооп-кампания
|
||||
net.join = Войти
|
||||
@@ -1075,12 +1088,20 @@ ability.drain.desc = лечит себя на треть нанесённого
|
||||
unit.giant = Великан
|
||||
unit.fairy = Фея-шпион
|
||||
unit.battlemage = Боевой маг
|
||||
unit.thief = Вор
|
||||
unit.monk = Монах
|
||||
unit.ranger = Следопыт
|
||||
unit.inquisitor = Инквизитор
|
||||
unit.ram = Таран
|
||||
unit.catapult = Катапульта
|
||||
msg.trap_wrecked = Герои сломали ловушку: {0}.
|
||||
best.giant.text = Великан - это то, что выходит, когда рыцарь триста лет ест овощи. Медленный, огромный, а двери для него - лишь пожелание. | Ломает ловушки, просто стоя рядом. Отвратительные манеры. Бей всем, что есть, лучше издалека.
|
||||
best.fairy.text = Маленькая доносчица Ордена: светящаяся мошка с крылышками, которую шлют вперёд найти твои ловушки и всем разболтать. Стоит ей увидеть ловушку, как герои её ломают. | Ещё и посыпает героев целебной пыльцой. Прихлопни её первой. Дважды, для верности.
|
||||
best.battlemage.text = Маг в мантии Ордена, для которого стены - личное оскорбление. Укреплённые стены лопаются от его огненных шаров, и он без конца об этом болтает. | Вблизи хрупок. Больше всего его бесят стальные стены - и хорошо.
|
||||
best.thief.text = Быстрые пальцы, ещё более быстрые ноги. Он пришёл не драться: прокрадывается в твою сокровищницу, набивает мешок и бежит обратно к вратам. | Убей его, пока он не ушёл, и золото снова твоё - с процентами.
|
||||
best.monk.text = Годы постов и молитв сделали его глухим к твоей магии: молния, мор и мороз стекают с него, как дождь с крыши. | Кулаки тоже бьют больно. Встречай его сталью, когтями и зубами.
|
||||
best.ranger.text = Лесничий Ордена, который стреляет издалека и расставляет железные капканы в твоих коридорах. Существо, наступившее в капкан, ранено и надолго застревает. | Капканы лежат, пока в них кто-нибудь не наступит. Лучше кобольд, чем твой огр.
|
||||
best.inquisitor.text = Дряхлый, но его проповеди опасны: каждые двадцать секунд он уговаривает одно из твоих самых слабых существ - кобольда или паука - перейти к героям. | Сильные существа над ним только смеются. Бей первым.
|
||||
best.ram.text = Бревно с железным наконечником и крышей от стрел. Ваши существа ему безразличны — только двери, стены и башни. | Остановите его до двери или заведите новую дверь.
|
||||
best.catapult.text = Деревянный рычаг, ведро камней и рыцарь с избытком свободного времени. Бьёт дальше ваших башен и целится в них первыми. | Атакуйте вплотную: вблизи она и мухи не обидит.
|
||||
|
||||
@@ -1264,6 +1285,7 @@ ed.terrain.ruby = Рубиновая жила
|
||||
ed.terrain.diamond = Алмазная жила
|
||||
ed.terrain.steel = Стальная стена
|
||||
ed.terrain.mushroom = Гигантский гриб
|
||||
ed.terrain.grove = Грибная роща
|
||||
ed.terrain.grass = Трава (поверхность)
|
||||
ed.terrain.tree = Дерево
|
||||
ed.place.core = Ядро души
|
||||
@@ -1346,6 +1368,8 @@ blk.reveal = Открыть
|
||||
blk.camera = Показать
|
||||
blk.gold = Золото +
|
||||
blk.mana = Мана +
|
||||
blk.weather = Погода на поверхности
|
||||
blk.talentxp = Опыт талантов +
|
||||
blk.if = Если
|
||||
blk.repeat = Повторить
|
||||
blk.trigger = Переключить триггер
|
||||
@@ -1365,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 = Включает триггер (он может сработать снова) или выключает его.
|
||||
@@ -1387,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 = в
|
||||
@@ -1443,6 +1470,7 @@ blk.until = Повторять, пока не
|
||||
blk.dountil = Повторять, затем проверить: пока не
|
||||
blk.wait = Ждать
|
||||
blk.w_seconds = сек.
|
||||
blk.w_for = на
|
||||
blk.help.until = Снова и снова выполняет блоки внутри, пока условие не станет верным; проверка перед каждым проходом, поэтому если оно уже верно, блоки не выполняются.
|
||||
blk.help.dountil = Выполняет блоки внутри, затем проверяет условие и повторяет, пока оно не станет верным; блоки выполняются хотя бы один раз.
|
||||
blk.help.wait = Приостанавливает этот скрипт на несколько секунд, затем выполняется следующий блок (и внутри циклов).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Кривопал: Ха! Слышал? С востока что-то
|
||||
tut.obj6 = Победи двух рыцарей.
|
||||
tut.knights = Кривопал: Два рыцаря! Пошли своих существ флагом атаки (Приказы) или порази их молнией.
|
||||
tut.win = Кривопал: Молодец, хозяин! Вот и всё. Ну — почти. Остальное расскажет кампания. Пойду поставлю чайник.
|
||||
|
||||
# master talents
|
||||
cat.master = Мастер
|
||||
talent.title = Таланты мастера
|
||||
talent.points = Свободные очки талантов: {0}
|
||||
talent.next = {0}/{1} опыта до следующего очка
|
||||
talent.all = Все таланты изучены.
|
||||
talent.builder = Строитель
|
||||
talent.warlord = Полководец
|
||||
talent.warlock = Чернокнижник
|
||||
talent.merchant = Торговец
|
||||
talent.builder.desc = Бесята копают на {0}% быстрее, комнаты стоят на {1}% меньше.
|
||||
talent.warlord.desc = Существа бьют на {0}% сильнее и получают на {1}% меньше урона.
|
||||
talent.warlock.desc = Мана течёт на {0}% быстрее, чары стоят на {1}% меньше.
|
||||
talent.merchant.desc = Жалованье на {0}% ниже, доставленные сокровища ценятся на {1}% выше.
|
||||
talent.none = Ещё не изучено.
|
||||
talent.then = Следующий уровень: {0}
|
||||
msg.talent_point = Новое очко таланта! Потрать его в окне мастера.
|
||||
msg.talent_learned = {0}: изучен уровень {1}.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Ясно
|
||||
weather.rain = Дождь
|
||||
weather.fog = Туман
|
||||
weather.storm = Гроза
|
||||
msg.weather_clear = Наверху проясняется.
|
||||
msg.weather_rain = Наверху начинается дождь - огонь не разгорается.
|
||||
msg.weather_fog = Туман стелется над землёй - снаружи все видят хуже.
|
||||
msg.weather_storm = Наверху надвигается гроза! Молнии бьют по бойцам и деревьям.
|
||||
msg.lightning_unit = Удар молнии: {0}!
|
||||
# creature names
|
||||
names.imp = Крошка, Сопля, Копуша, Грязнуля, Чумазик, Шкет, Рылька, Замарашка, Козявка, Царапка, Комок, Хрюндель, Плюх, Пачкун, Лопатка, Шмыга, Нюхля, Мелочь, Ковырь, Бултых
|
||||
names.kobold = Тявка, Щипун, Зубило, Паршивец, Хвать, Ржавчик, Шмыг, Кусака, Разиня, Крыс, Лохмотик, Колючка, Коготь, Склочник, Корешок, Хихикс, Пихало, Зазубрина, Хрясь, Царапыч
|
||||
names.spider = Шуршик, Мохнолап, Паучок, Щекотун, Сеточка, Восьминог, Ниточка, Жуткоша, Перебирка, Шёлковка, Пряха, Прыгуля, Пушистик, Текла, Катушка, Хватайка, Клубочек, Липучка, Вертушка, Иголочка
|
||||
names.mage = Варфоломей, Игнатий, Леопольд, Феофан, Евлампий, Иероним, Аполлинарий, Пафнутий, Корнилий, Онуфрий, Флорентий, Модест, Панкратий, Евстафий, Константин, Евсевий, Северин, Варнава, Акакий, Ферапонт
|
||||
names.ogre = Громила, Чурбан, Бумбум, Валун, Тупица, Дубина, Бухтя, Колотун, Топтыга, Пень, Плюх, Брюхан, Балда, Пузан, Дубинка, Бугай, Грохот, Глыба, Барабан, Горбо
|
||||
names.jailer = Хмурый, Засов, Кандальник, Замок, Темница, Железнобород, Скоба, Болт, Суровый, Решёткин, Насупленный, Крюк, Петля, Ключник, Мрачный, Звякун, Колодник, Стражник, Клеть, Горький
|
||||
names.priestess = Жара, Пламя, Пепелина, Серафима, Полымя, Огнеслава, Морана, Брунгильда, Игния, Искра, Багряна, Жарка, Ворона, Лилит, Лучина, Изольда, Огнеяра, Никта, Вечерня, Купава
|
||||
names.chainbreaker = Цепелом, Ульфгар, Железнодух, Браннок, Крагмор, Вальдрек, Терн, Горат, Несломленный, Рюрик, Торвальд, Хаген, Дурнок, Осколок, Пламень, Волкослав, Коррун, Арвак, Освободитель, Хальвард
|
||||
names.bat = Писк, Порх, Пипа, Шорох, Эхо, Махалка, Сумрак, Свистун, Цвирк, Зубик, Вихрь, Нетопырь, Моль, Пискля, Шурша, Висюлька, Бархат, Шмыгун, Визгля, Молния
|
||||
names.gargoyle = Гранит, Сланец, Базальт, Каменюка, Раствор, Галька, Щебень, Кремень, Шов, Руина, Кварц, Пемза, Карниз, Нарост, Каменобород, Угрюм, Фронтон, Водосток, Долото, Цоколь
|
||||
names.owl = Филин, Умник, Пёрышко, Бартольд, Минерва, Товий, Ухух, Совушка, Пухлик, Вздох, Мыслитель, Старейшина, Сыч, Ноктуа, Совуслав, Пергамент, Сумеречник, Неясыть, Профессор, Пушок
|
||||
names.wyrm = Вермитракс, Пеплопасть, Палёный, Кальдерон, Игнивар, Жаророг, Драккар, Пирр, Обсидиан, Жаропасть, Вулканор, Палезуб, Малграт, Тиракс, Инферно, Сера, Кальдор, Фафнир, Рагнор, Уголёк
|
||||
names.cogling = Шестерёнка, Колёсико, Винтик, Трещотка, Заклёпка, Привод, Мастерок, Штуковина, Ключик, Шпиндель, Звякалка, Ковыряшка, Гаечка, Жужжа, Рычажок, Юла, Фланец, Щёлк, Тиктак, Шурупчик
|
||||
names.revenant = Тоска, Горечь, Пепельный, Стон, Пустота, Скорбь, Бледный, Кручина, Тень, Прах, Могильщик, Сумрак, Склеп, Печаль, Бдение, Курган, Саван, Элегия, Осадок, Пустошь
|
||||
names.beast = Лохмач, Мишка, Бруно, Косматый, Чавка, Мохоспин, Ворчун, Медвежонок, Кустик, Хрум, Щёлкун, Ползун, Щетина, Мох, Рокот, Ежевика, Стрекот, Бурчун, Топтыгин, Нюхач
|
||||
names.human = Жерар, Роланд, Конрад, Альберт, Тристан, Экхарт, Гавейн, Ланселот, Парцифаль, Гуго, Бертрам, Готфрид, Ортвин, Балдуин, Рейнольд, Вильфрид, Дитрих, Жильбер, Руперт, Ратибор
|
||||
epithets.mid = Костолом, Грязнопят, Крысогрыз, Чумазорыл, Камнежуй, Черепокол, Смердокулак, Ржавокост, Брюхобой, Лягун, Плеснебород, Грязекоготь, Соплемёт, Ухогрыз, Коленолом, Червеед
|
||||
epithets.high = Цареубийца, Рокорук, Ужасозуб, Мирокрушитель, Героебой, Бурекулак, Ночестрах, Душежнец, Железнодлань, Градолом, Драконобой, Адоход, Теневладыка, Крововенец, Рокопад, Звездолом
|
||||
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 = гибель в бою
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Brána sa rúca! Objavuje sa strážca brány s obrancami ({0})!
|
||||
msg.gate_defenders = Brána povoláva obrancov ({0})!
|
||||
msg.level_up = {0} dosahuje úroveň {1}.
|
||||
msg.hero_slain = Porazený: {0}! Korisť: {1} zlata.
|
||||
msg.thief_in_treasury = V tvojej pokladnici je zlodej!
|
||||
msg.thief_escaped = Zlodej ušiel s {0} zlata!
|
||||
msg.snare_caught = Do pasce stúpil: {0}!
|
||||
msg.converted = Inkvizítor obrátil jedného z tvojich tvorov: {0}!
|
||||
msg.creature_died = Padol tvoj tvor: {0}.
|
||||
msg.portal_claimed = Portál je tvoj! Priťahuje tvory.
|
||||
msg.flag_removed = Útočná vlajka odstránená. Tvoje tvory sa vracajú.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Nedostatok many (treba {0}).
|
||||
hint.portal_no_lair = Portál je otvorený, ale bez brlohu žiadne tvory neprídu.
|
||||
hint.no_farm = Tvory chcú jesť: postav hubovú záhradu.
|
||||
hint.lair_full = Všetky lôžka v brlohu sú obsadené. Zväčši brloh.
|
||||
hint.creature_max = Tvoja kobka má 100 tvorov - cez portál už ďalšie neprídu.
|
||||
hint.gate_attack = Tvoje tvory útočia na bránu hrdinov!
|
||||
hint.treasury_full = Tvoje pokladnice sú plné! Postav ďalšie políčka pokladnice.
|
||||
hint.flag_unreachable = Tvoje tvory sa k útočnej vlajke nedostanú.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Pravé kliknutie Položiť jednotku (zajatcov aj do mučiarne)
|
||||
help.k_slap = Shift+Ľavé Ruka: zaucho vlastným, päsť hrdinom na tvojom území
|
||||
help.k_pan = Pravé ťahanie Posun mapy (aj WASD/šípky, okraj obrazovky)
|
||||
help.k_rot = Stredné ťahanie Otáčanie a nakláňanie kamery (aj Q/E, R/F)
|
||||
help.k_reset = Stredné 2x klik Obnoviť uhol a otočenie (miesto zostane)
|
||||
help.k_zoom = Koliesko myši Zoom
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Voľba nástroja
|
||||
help.k_misc = P / Medzerník Pauza M Zvuk zap./vyp. +/- Hlasitosť
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Čo do nej spadne, zhorí. Prechádzajú ju kamenné mosty.
|
||||
tile.bridge = Drevený most
|
||||
tile.lavabridge = Kamenný most
|
||||
tile.mushroom = Obria huba
|
||||
tile.mushroom_grown = Dorastená: pripravená na vyťaženie.
|
||||
tile.mushroom_growing = Rastie: {0}% - vyťažiť sa dá až dorastená.
|
||||
tile.grove = Hubový háj
|
||||
tile.grove_info = Tento mach nikto nedobyje. Obrie huby tu znova dorastajú.
|
||||
tile.tree = Strom
|
||||
tile.plant_info = Označ krompáčom: škriatok ho vyrúbe za {0} dreva.
|
||||
tile.drawbridge = Padací most
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Otvorené zápasy
|
||||
net.no_rooms = Žiadne otvorené zápasy.
|
||||
net.create = Vytvoriť zápas
|
||||
net.map_size = Mapa {0}x{1} · {2} hráči
|
||||
net.coop_mission = Kooperatívna kampaň: misia {0} ({1})
|
||||
net.coop_create = Kooperácia
|
||||
net.coop_editor = Kampaň z editora
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = lieči sa o tretinu poškodenia, ktoré spôsobí.
|
||||
unit.giant = Obor
|
||||
unit.fairy = Víla špiónka
|
||||
unit.battlemage = Bojový mág
|
||||
unit.thief = Zlodej
|
||||
unit.monk = Mních
|
||||
unit.ranger = Hraničiar
|
||||
unit.inquisitor = Inkvizítor
|
||||
unit.ram = Baranidlo
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Hrdinovia zničili pascu: {0}.
|
||||
best.giant.text = Obor je to, čo sa stane, keď rytier tristo rokov je zeleninu. Pomalý, obrovský a dvere berie len ako návrh. | Pasce ničí už tým, že stojí vedľa nich. Odporné spôsoby. Bi ho všetkým, čo máš, najlepšie z diaľky.
|
||||
best.fairy.text = Malý udavač Rádu: žiariaci komár s krídlami, poslaný napred, aby našiel tvoje pasce a všetkým to vytáral. Len čo pascu uvidí, hrdinovia ju rozbijú. | Navyše sype na hrdinov liečivý prach. Plesni ju prvú. Dvakrát, pre istotu.
|
||||
best.battlemage.text = Mág v rúchu Rádu, ktorý považuje múry za osobnú urážku. Spevnené steny pod jeho ohnivými guľami praskajú a on o tom neprestane rozprávať. | Zblízka krehký. Najviac ho štvú oceľové steny - dobre.
|
||||
best.thief.text = Rýchle prsty, ešte rýchlejšie nohy. Neprichádza bojovať: vkradne sa do tvojej pokladnice, naplní si vrece a uteká späť k bráne. | Zabi ho skôr, než ujde, a zlato je zase tvoje - aj s úrokmi.
|
||||
best.monk.text = Roky pôstu a modlitieb ho urobili hluchým k tvojej mágii: blesk, mor aj mráz z neho stekajú ako dážď zo strechy. | Aj päste bolia. Priviť ho oceľou, pazúrmi a zubami.
|
||||
best.ranger.text = Lesník Rádu, ktorý strieľa z diaľky a nastražuje v tvojich chodbách železné pasce. Tvor, ktorý do jednej stúpi, je zranený a chvíľu uviazne. | Pasce zostávajú, kým do nich niekto nestúpi. Radšej kobold než tvoj zlobor.
|
||||
best.inquisitor.text = Krehký, ale jeho kázne sú nebezpečné: každých dvadsať sekúnd prehovorí jedného z tvojich najslabších tvorov - kobolda alebo pavúka -, aby prešiel k hrdinom. | Silní tvorovia sa mu len vysmejú. Udri prvý.
|
||||
best.ram.text = Kmeň so železnou hlavicou a strieškou proti šípom. Tvoje bytosti ho nezaujímajú – tvoje dvere, múry a veže áno. | Zastav ho pred dverami, alebo si zaobstaraj nové.
|
||||
best.catapult.text = Drevené rameno, vedro kameňov a rytier s priveľa voľným časom. Strieľa ďalej ako tvoje veže a mieri najprv na ne. | Vrhni sa naň: zblízka neublíži ani muche.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Rubínová žila
|
||||
ed.terrain.diamond = Diamantová žila
|
||||
ed.terrain.steel = Oceľová stena
|
||||
ed.terrain.mushroom = Obria huba
|
||||
ed.terrain.grove = Hubový háj
|
||||
ed.terrain.grass = Tráva (povrch)
|
||||
ed.terrain.tree = Strom
|
||||
ed.place.core = Jadro duše
|
||||
@@ -1354,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č
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Krivoprst: Ha! Počul si? Niečo prichádza z východu. Priprav sa: b
|
||||
tut.obj6 = Poraz dvoch rytierov.
|
||||
tut.knights = Krivoprst: Dvaja rytieri! Pošli na nich tvory útočnou vlajkou (Rozkazy) alebo ich zasiahni bleskom.
|
||||
tut.win = Krivoprst: Výborne, pane! To je všetko. No - takmer. Zvyšok povie kampaň. Pristavím vodu na čaj.
|
||||
|
||||
# master talents
|
||||
cat.master = Pán
|
||||
talent.title = Pánove talenty
|
||||
talent.points = Voľné body talentov: {0}
|
||||
talent.next = {0}/{1} skúseností do ďalšieho bodu
|
||||
talent.all = Všetky talenty sú naučené.
|
||||
talent.builder = Staviteľ
|
||||
talent.warlord = Vojvodca
|
||||
talent.warlock = Černokňažník
|
||||
talent.merchant = Obchodník
|
||||
talent.builder.desc = Čertíci kopú o {0}% rýchlejšie, miestnosti stoja o {1}% menej.
|
||||
talent.warlord.desc = Tvory udierajú o {0}% silnejšie a dostávajú o {1}% menej poškodenia.
|
||||
talent.warlock.desc = Mana prúdi o {0}% rýchlejšie, kúzla stoja o {1}% menej.
|
||||
talent.merchant.desc = Mzdy sú o {0}% nižšie, doručené poklady majú o {1}% vyššiu hodnotu.
|
||||
talent.none = Zatiaľ nenaučené.
|
||||
talent.then = Ďalšia úroveň: {0}
|
||||
msg.talent_point = Nový bod talentu! Použi ho v okne pána.
|
||||
msg.talent_learned = {0}: úroveň {1} naučená.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Jasno
|
||||
weather.rain = Dážď
|
||||
weather.fog = Hmla
|
||||
weather.storm = Búrka
|
||||
msg.weather_clear = Na povrchu sa vyjasňuje.
|
||||
msg.weather_rain = Na povrchu začína pršať - oheň sa nechytí.
|
||||
msg.weather_fog = Nad krajinou sa valí hmla - vonku každý vidí menej.
|
||||
msg.weather_storm = Na povrchu sa blíži búrka! Blesky zasahujú jednotky a stromy.
|
||||
msg.lightning_unit = Zásah bleskom: {0}!
|
||||
# creature names
|
||||
names.imp = Omrvinka, Sopliak, Hrabko, Blatko, Špinďúr, Prcek, Rýpal, Čuňo, Smradík, Škrabko, Hruda, Čľapka, Mazúr, Fľakáč, Hrudka, Lopatka, Ňufák, Drobček, Šmudlo, Čmudo
|
||||
names.kobold = Ňafko, Štípal, Zubáč, Prašivec, Hryzko, Hrdzák, Šmyk, Kúsok, Čumil, Potkan, Handrár, Bodliak, Pazúrik, Rýpavec, Korienok, Chichot, Strkal, Zúbok, Chrup, Škrabák
|
||||
names.spider = Lezúň, Chlpka, Pavúčik, Štekliak, Sieťka, Osemnôžka, Niťka, Strašidielko, Šmatlík, Hodvábka, Tkáčka, Skokan, Chlpáčik, Tekla, Cievka, Chňapka, Klbko, Lepka, Vrtko, Ihlička
|
||||
names.mage = Bartolomej, Ignác, Leopold, Teobald, Vojtech, Hieronym, Valdemar, Gotfríd, Kornel, Otokar, Florián, Engelbert, Pankrác, Ruprecht, Konštantín, Euzébius, Servác, Barnabáš, Archibald, Bonifác
|
||||
names.ogre = Hromotĺk, Peň, Bum, Balvan, Tupko, Klát, Buchal, Mlátič, Dupák, Drevák, Čľup, Bachor, Kváder, Bruchoš, Kyjak, Vôl, Rachot, Kus, Bubon, Gorbo
|
||||
names.jailer = Zamračenec, Závora, Okov, Zámok, Žalár, Železobrad, Krúžok, Skrutka, Prísny, Mrežník, Mrzút, Hák, Slučka, Kľučiar, Temný, Rinčal, Klada, Strážnik, Klietka, Horký
|
||||
names.priestess = Žeravá, Plamena, Popolica, Serafína, Vatra, Ohnivka, Morena, Brunhilda, Ignia, Iskra, Šarlátka, Žiarka, Havranka, Lilit, Trúd, Izolda, Planúca, Nyx, Večernica, Plamenná
|
||||
names.chainbreaker = Reťazolam, Ulfgar, Železoduch, Brannok, Kragmor, Valdrek, Tŕň, Gorath, Nespútaný, Rurik, Torvald, Hagen, Durnok, Črep, Plameň, Vlkoslav, Korrun, Arvak, Osloboditeľ, Halvard
|
||||
names.bat = Pišťal, Trepotal, Pípa, Šuchot, Ozvena, Mávalka, Súmrak, Pišťalka, Cvrlík, Zúbik, Vír, Netopierik, Moľa, Pípaľ, Šumko, Visáčik, Zamat, Šmyk, Kvičal, Blesk
|
||||
names.gargoyle = Žula, Bridlica, Čadič, Kamenár, Malta, Okruhliak, Štrk, Kremeň, Škára, Ruina, Kremenec, Pemza, Rímsa, Hrča, Kamenobrad, Bručúň, Štít, Chrlič, Dláto, Sokel
|
||||
names.owl = Výr, Múdrik, Brko, Bartold, Minerva, Tobiáš, Húhú, Sovička, Pierko, Povzdych, Mysliteľ, Starešina, Kuvik, Noctua, Sovoslav, Pergamen, Šero, Plamienka, Profesor, Páperie
|
||||
names.wyrm = Vermitrax, Popolochrtan, Spáleniak, Kalderon, Ignivar, Žeroroh, Drakkar, Pyrrhos, Obsidián, Žeravochrtan, Vulkanor, Pálizub, Malgrath, Tyrax, Inferno, Síra, Kaldor, Fafnir, Ragnor, Uhlík
|
||||
names.cogling = Ozubák, Koliesko, Skrutôčka, Rapkáč, Nitík, Prevod, Kutil, Vynálezík, Kľúčik, Pastorok, Cinkal, Šťúral, Matička, Bzučák, Kľučka, Vrtuľka, Príruba, Cvak, Tiktak, Vrut
|
||||
names.revenant = Clivota, Horkosť, Popolavý, Ston, Prázdno, Žiaľ, Bledý, Trud, Tieň, Prach, Hrobár, Súmrak, Krypta, Túžba, Bdenie, Mohyla, Rubáš, Elégia, Kal, Pustatina
|
||||
names.beast = Chlpáč, Maco, Bruno, Huňáč, Mľaskal, Machoň, Bručko, Medvedík, Krovie, Chrúm, Cvakal, Lezec, Štetinka, Mach, Burácal, Ostružina, Cvrkal, Brblal, Brumko, Ňuchal
|
||||
names.human = Gerhard, Roland, Konrád, Albrecht, Tristan, Eckhart, Gawain, Lancelot, Parsifal, Hugo, Bertram, Gotfríd, Ortvín, Balduin, Rajnold, Vilfríd, Dietrich, Gilbert, Ruprecht, Bystrík
|
||||
epithets.mid = Kostilam, Blatonoh, Potkanohryz, Špinotvár, Kamenožrút, Lebkolam, Smradopäsť, Hrdzokĺb, Bruchobij, Holeňokop, Plesňobrad, Špinopazúr, Soplomet, Uchohryz, Kolenolam, Červojed
|
||||
epithets.high = Kráľovrah, Skazopäsť, Hrôzozub, Svetoborec, Hrdinobij, Búrkopäsť, Morovládca, Dušovrah, Železosrdce, Hradoborec, Drakobijec, Pekelník, Tieňovládca, Krvokorunec, Skazotok, Hviezdolam
|
||||
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
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Капија се руши! Појављује се Чувар
|
||||
msg.gate_defenders = Капија призива браниоце: {0}!
|
||||
msg.level_up = {0} достиже ниво {1}.
|
||||
msg.hero_slain = Поражен: {0}! Плен: {1} злата.
|
||||
msg.thief_in_treasury = Лопов је у твојој ризници!
|
||||
msg.thief_escaped = Лопов је побегао са {0} злата!
|
||||
msg.snare_caught = Стало је у замку: {0}!
|
||||
msg.converted = Инквизитор је преобратио једно твоје створење: {0}!
|
||||
msg.creature_died = Пало је твоје створење: {0}.
|
||||
msg.portal_claimed = Портал је твој! Привлачи створења.
|
||||
msg.flag_removed = Застава напада уклоњена. Створења се враћају.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Нема довољно мане (потребно {0}).
|
||||
hint.portal_no_lair = Портал је отворен, али без брлога ниједно створење неће доћи.
|
||||
hint.no_farm = Створења желе да једу: изгради врт печурака.
|
||||
hint.lair_full = Сви кревети у брлогу су заузети. Прошири брлог.
|
||||
hint.creature_max = Твоја тамница има 100 створења - кроз портал их више не долази.
|
||||
hint.gate_attack = Твоја створења нападају Капију хероја!
|
||||
hint.treasury_full = Ризнице су пуне! Изгради још поља ризнице.
|
||||
hint.flag_unreachable = Твоја створења не могу да стигну до заставе напада.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Десни клик Спусти јединицу (заро
|
||||
help.k_slap = Shift+леви Рука: ошамари своје јединице, удари хероје на свом тлу
|
||||
help.k_pan = Десно превлачење Помера мапу (и WASD/стрелице, ивица екрана)
|
||||
help.k_rot = Средње превлачење Окреће и нагиње камеру (и Q/E, R/F)
|
||||
help.k_reset = Средњи 2x клик Врати угао и окрет (место остаје)
|
||||
help.k_zoom = Точкић миша Зумирање
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Избор алатке
|
||||
help.k_misc = P / Размак Пауза M Звук ук./иск. +/- Јачина
|
||||
@@ -258,6 +264,8 @@ val.medium = Средње
|
||||
val.high = Високо
|
||||
val.normal = Нормално
|
||||
val.large = Велико
|
||||
val.small = Мало
|
||||
val.auto_plain = Аутоматски
|
||||
|
||||
# ---- key binding names
|
||||
key.cam_forward = Камера напред
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Шта год упадне, изгори. Камени мост
|
||||
tile.bridge = Дрвени мост
|
||||
tile.lavabridge = Камени мост
|
||||
tile.mushroom = Џиновска печурка
|
||||
tile.mushroom_grown = Одрасла: спремна за сечу.
|
||||
tile.mushroom_growing = Расте: {0}% - може се посећи тек кад одрасте.
|
||||
tile.grove = Гај печурака
|
||||
tile.grove_info = Нико не може да освоји ову маховину. Овде поново расту џиновске печурке.
|
||||
tile.tree = Стабло
|
||||
tile.plant_info = Означи крампом: враг је сруши за {0} дрва.
|
||||
tile.drawbridge = Покретни мост
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Отворене партије
|
||||
net.no_rooms = Нема отворених партија.
|
||||
net.create = Нова партија
|
||||
net.map_size = Мапа {0}x{1} · {2} играча
|
||||
net.coop_mission = Ко-оп кампања: мисија {0} ({1})
|
||||
net.coop_create = Ко-оп кампања
|
||||
net.coop_editor = Кампања уређивача
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = лечи се за трећину штете коју на
|
||||
unit.giant = Џин
|
||||
unit.fairy = Вила ухода
|
||||
unit.battlemage = Борбени маг
|
||||
unit.thief = Лопов
|
||||
unit.monk = Монах
|
||||
unit.ranger = Ловац
|
||||
unit.inquisitor = Инквизитор
|
||||
unit.ram = Ован
|
||||
unit.catapult = Катапулт
|
||||
msg.trap_wrecked = Хероји су уништили замку: {0}.
|
||||
best.giant.text = Џин је оно што се деси кад витез триста година једе поврће. Спор, огроман, а врата сматра предлогом. | Замке уништава самим тим што стоји поред њих. Одвратно понашање. Удари га свиме, најбоље издалека.
|
||||
best.fairy.text = Мали доушник Реда: светлећа мушица с крилима, послата напред да пронађе твоје замке и свима их ода. Кад једном види замку, хероји је разбију. | Уз то посипа хероје лековитим прахом. Згњечи њу прву. Двапут, за сваки случај.
|
||||
best.battlemage.text = Маг у одежди Реда који зидове сматра личном увредом. Ојачани зидови пуцају под његовим ватреним куглама, а он о томе непрестано прича. | Крхак изблиза. Челични зидови га највише нервирају - добро.
|
||||
best.thief.text = Брзи прсти, још брже ноге. Не долази да се бори: ушуња се у твоју ризницу, напуни врећу и отрчи назад до капије. | Убиј га пре него што побегне и злато је опет твоје - с каматом.
|
||||
best.monk.text = Године поста и молитве учиниле су га глувим за твоју магију: муња, куга и мраз клизе с њега као киша с крова. | И песнице боле. Дочекај га челиком, канџама и зубима.
|
||||
best.ranger.text = Шумар Реда који гађа издалека и оставља гвоздене замке у твојим ходницима. Створење које стане у једну рањено је и неко време заглављено. | Замке остају док неко не стане у њих. Боље коболд него твој огр.
|
||||
best.inquisitor.text = Крхак, али су му проповеди опасне: на сваких двадесет секунди наговори једно од твојих најслабијих створења - коболда или паука - да пређе херојима. | Јака створења му се само смеју. Удари први.
|
||||
best.ram.text = Дебло са гвозденом капом и кровом против стрела. Твоја створења га не занимају – твоја врата, зидови и куле да. | Заустави га пре врата или набави нова врата.
|
||||
best.catapult.text = Дрвена рука, канта камења и витез са превише слободног времена. Гађа даље од твојих кула и циља их прве. | Јуриш на њега: изблиза ни мушици не нашкоди.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Жила рубина
|
||||
ed.terrain.diamond = Жила дијаманата
|
||||
ed.terrain.steel = Челични зид
|
||||
ed.terrain.mushroom = Џиновска печурка
|
||||
ed.terrain.grove = Гај печурака
|
||||
ed.terrain.grass = Трава (површина)
|
||||
ed.terrain.tree = Стабло
|
||||
ed.place.core = Језгро душе
|
||||
@@ -1354,6 +1376,8 @@ blk.reveal = Откриј
|
||||
blk.camera = Покажи
|
||||
blk.gold = Злато +
|
||||
blk.mana = Мана +
|
||||
blk.weather = Време на површини
|
||||
blk.talentxp = Искуство талената +
|
||||
blk.if = Ако
|
||||
blk.repeat = Понови
|
||||
blk.trigger = Пребаци окидач
|
||||
@@ -1373,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 = Укључује окидач (може поново да се активира) или га искључује.
|
||||
@@ -1395,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 = у
|
||||
@@ -1443,6 +1470,7 @@ blk.until = Понављај док
|
||||
blk.dountil = Понови, затим провери: док
|
||||
blk.wait = Чекај
|
||||
blk.w_seconds = секунди
|
||||
blk.w_for = на
|
||||
blk.help.until = Покреће унутрашње блокове изнова и изнова док услов не почне да важи - проверава се пре сваког пролаза, па ако већ важи, унутрашњост се уопште не покреће.
|
||||
blk.help.dountil = Покреће унутрашње блокове, затим проверава услов и понавља док не почне да важи - унутрашњост се увек покрене бар једном.
|
||||
blk.help.wait = Паузира ову скрипту неколико секунди, а затим наставља следећим блоком (и унутар петљи).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Кривопрст: Ха! Јеси ли чуо? Нешто долаз
|
||||
tut.obj6 = Порази два витеза.
|
||||
tut.knights = Кривопрст: Два витеза! Пошаљи створења заставом напада (Наредбе) или их удари муњом.
|
||||
tut.win = Кривопрст: Браво, господару! То је све. Па - скоро. Остатак ће испричати кампања. Идем да ставим воду за чај.
|
||||
|
||||
# master talents
|
||||
cat.master = Господар
|
||||
talent.title = Господареви таленти
|
||||
talent.points = Слободни бодови талента: {0}
|
||||
talent.next = {0}/{1} искуства до следећег бода
|
||||
talent.all = Сви таленти су научени.
|
||||
talent.builder = Градитељ
|
||||
talent.warlord = Војсковођа
|
||||
talent.warlock = Вештац
|
||||
talent.merchant = Трговац
|
||||
talent.builder.desc = Ђаволци копају {0}% брже, собе коштају {1}% мање.
|
||||
talent.warlord.desc = Створења ударају {0}% јаче и примају {1}% мање штете.
|
||||
talent.warlock.desc = Мана тече {0}% брже, чини коштају {1}% мање.
|
||||
talent.merchant.desc = Плате су {0}% ниже, достављено благо вреди {1}% више.
|
||||
talent.none = Још није научено.
|
||||
talent.then = Следећи ниво: {0}
|
||||
msg.talent_point = Нови бод талента! Потроши га у прозору господара.
|
||||
msg.talent_learned = {0}: научен ниво {1}.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Ведро
|
||||
weather.rain = Киша
|
||||
weather.fog = Магла
|
||||
weather.storm = Олуја
|
||||
msg.weather_clear = На површини се разведрава.
|
||||
msg.weather_rain = На површини почиње киша - ватра се не хвата.
|
||||
msg.weather_fog = Магла се спушта над земљу - напољу сви виде мање.
|
||||
msg.weather_storm = На површини се спрема олуја! Муње погађају јединице и стабла.
|
||||
msg.lightning_unit = Муња је погодила: {0}!
|
||||
# creature names
|
||||
names.imp = Мрвица, Шмркљо, Копко, Блатко, Прљко, Чупко, Ровко, Гњецко, Смрдко, Гребко, Квргица, Брља, Мазало, Пацко, Грудвица, Лопатко, Њушко, Копуша, Мрљко, Шљампа
|
||||
names.kobold = Кевко, Штипко, Зубоња, Шуго, Гризо, Рђо, Фића, Угризић, Зјало, Пацов, Крпо, Бодљо, Канџо, Гњаватор, Корен, Кикотко, Гурко, Зарез, Крцко, Огребо
|
||||
names.spider = Гмизалица, Вунко, Паучић, Шкакљо, Мрежица, Осмонога, Нитица, Језица, Прчко, Свиленка, Ткаља, Скокић, Длакавко, Текла, Калемко, Хватко, Клупко, Лепко, Вртко, Иглица
|
||||
names.mage = Вартоломеј, Игњатије, Леополд, Теобалд, Адалберт, Јероним, Валдемар, Готфрид, Корнелије, Отокар, Флоријан, Енгелберт, Панкратије, Рупрехт, Константин, Јевсевије, Северин, Варнава, Арчибалд, Витомир
|
||||
names.ogre = Громо, Клада, Бумбо, Грудо, Тупко, Глупко, Треско, Батина, Газило, Тикван, Пљус, Дебељко, Бухто, Трбоња, Тољага, Волина, Тутњо, Кврго, Бубањ, Горбо
|
||||
names.jailer = Мрки, Засун, Оков, Катанац, Тамница, Гвозденбрад, Карика, Сворњак, Строги, Решетко, Намргођен, Кука, Омча, Кључар, Мрачни, Звекан, Букагија, Стражар, Кавез, Горки
|
||||
names.priestess = Жар, Пламена, Пепела, Серафина, Буктиња, Ватрена, Морана, Брунхилда, Игнија, Искра, Гримизна, Жарка, Гавранка, Лилит, Труд, Изолда, Огњена, Никс, Вечерња, Пламенка
|
||||
names.chainbreaker = Ломиланац, Улфгар, Гвоздендух, Бранок, Крагмор, Валдрек, Трн, Горат, Неспутани, Рурик, Торвалд, Хаген, Дурнок, Крхотина, Пламен, Вукослав, Корун, Арвак, Ослободилац, Халвард
|
||||
names.bat = Пијук, Лепршо, Цико, Шушур, Јека, Махалица, Сумрак, Звиждо, Цврчко, Зубић, Вртлог, Слепишић, Мољац, Пијукић, Шушо, Виско, Сомот, Шмуго, Цича, Муња
|
||||
names.gargoyle = Гранит, Шкриљац, Базалт, Каменко, Малтер, Шљунко, Туцаник, Кремен, Фуга, Рушевина, Кварц, Пловућац, Венац, Квргоња, Каменобрад, Мрзовоља, Забат, Ригало, Длето, Постоље
|
||||
names.owl = Ћук, Мудрица, Перце, Бартолд, Минерва, Товија, Хуху, Совица, Перић, Уздах, Мислилац, Старешина, Јејина, Ноктуа, Совослав, Пергамент, Сутон, Ушара, Професор, Паперје
|
||||
names.wyrm = Вермитракс, Пепељави, Палеж, Калдерон, Игнивар, Жаророг, Дракар, Пир, Опсидијан, Жароуст, Вулканор, Пализуб, Малграт, Тиракс, Инферно, Сумпор, Калдор, Фафнир, Рагнор, Угарак
|
||||
names.cogling = Зупчаник, Точкић, Шрафић, Чегртаљка, Закивак, Пренос, Мајсторко, Справица, Кључић, Погон, Звецкало, Петљо, Клинчић, Зујо, Ручица, Чигра, Прирубница, Шкљоц, Тиктак, Навојко
|
||||
names.revenant = Сета, Горчина, Пепељко, Јаук, Шупаљ, Туга, Бледи, Јад, Сенка, Прах, Гробар, Смркнуће, Крипта, Чежња, Бдење, Хумка, Покров, Елегија, Талог, Пустош
|
||||
names.beast = Чупавко, Медо, Бруно, Рундо, Мљацко, Маховинко, Мрмљо, Медведић, Грмко, Хрско, Шкљоцко, Гмизо, Чекињо, Маховина, Тутањ, Купина, Цврчо, Гунђало, Медоња, Њушкало
|
||||
names.human = Жерар, Роланд, Конрад, Алберт, Тристан, Екхарт, Гавин, Ланселот, Парсифал, Хуго, Бертрам, Готфрид, Ортвин, Балдуин, Рајнолд, Вилфрид, Дитрих, Жилбер, Рупрехт, Драгутин
|
||||
epithets.mid = Ломикост, Блатоног, Пацовогриз, Прљолица, Каменождер, Лобањолом, Смрдошака, Рђозглоб, Трбухобој, Голенобој, Буђобрад, Прљоканџа, Шмркљобацач, Ухогриз, Коленолом, Црвождер
|
||||
epithets.high = Краљоубица, Кобошака, Страхозуб, Светолом, Јунакомор, Олујошака, Ноћнострах, Душожетелац, Гвожђесрце, Градолом, Змајоубица, Пакленоход, Сеновладар, Крвокрун, Кобноплима, Звездолом
|
||||
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 = погибија у боју
|
||||
@@ -64,6 +64,10 @@ msg.gate_boss = Porten vacklar! Portväktaren dyker upp med {0} försvarare!
|
||||
msg.gate_defenders = Porten kallar på {0} försvarare!
|
||||
msg.level_up = {0} når nivå {1}.
|
||||
msg.hero_slain = {0} besegrad! Byte: {1} guld.
|
||||
msg.thief_in_treasury = En tjuv är i din skattkammare!
|
||||
msg.thief_escaped = En tjuv kom undan med {0} guld!
|
||||
msg.snare_caught = Klev i en fälla: {0}!
|
||||
msg.converted = Inkvisitorn har omvänt en av dina varelser: {0}!
|
||||
msg.creature_died = Din {0} har stupat.
|
||||
msg.portal_claimed = Portalen är din! Den lockar till sig varelser.
|
||||
msg.flag_removed = Anfallsflaggan borttagen. Dina varelser återvänder.
|
||||
@@ -81,6 +85,7 @@ msg.no_mana_cost = Inte tillräckligt med mana ({0} krävs).
|
||||
hint.portal_no_lair = Portalen är öppen, men utan näste kommer inga varelser.
|
||||
hint.no_farm = Varelserna vill äta: bygg en svampträdgård.
|
||||
hint.lair_full = Alla sängar i nästet är upptagna. Gör nästet större.
|
||||
hint.creature_max = Din fängelsehåla rymmer 100 varelser - inga fler kommer genom portalen.
|
||||
hint.gate_attack = Dina varelser anfaller Hjälteporten!
|
||||
hint.treasury_full = Dina skattkammare är fulla! Bygg fler skattkammarrutor.
|
||||
hint.flag_unreachable = Dina varelser kan inte nå anfallsflaggan.
|
||||
@@ -154,6 +159,7 @@ help.k_right = Högerklick Släpp en enhet (fångar: även i tortyrkammaren
|
||||
help.k_slap = Shift+Vänster Hand: örfila egna enheter, slå hjältar på din mark
|
||||
help.k_pan = Högerdrag Flytta kartan (även WASD/pilar, skärmkanten)
|
||||
help.k_rot = Mittendrag Rotera och luta kameran (även Q/E, R/F)
|
||||
help.k_reset = Mitten 2x klick Återställ vinkel och vridning (platsen kvar)
|
||||
help.k_zoom = Mushjul Zooma
|
||||
help.k_tools = 1-9,0,X,V,K,J,B,C,T,U,Y,O Välj verktyg
|
||||
help.k_misc = P / Mellanslag Paus M Ljud på/av +/- Volym
|
||||
@@ -258,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
|
||||
@@ -460,6 +468,10 @@ tile.lava_info = Det som faller i brinner upp. Stenbroar korsar den.
|
||||
tile.bridge = Träbro
|
||||
tile.lavabridge = Stenbro
|
||||
tile.mushroom = Jättesvamp
|
||||
tile.mushroom_grown = Fullvuxen: redo att fällas.
|
||||
tile.mushroom_growing = Växer: {0}% - kan bara fällas när den är fullvuxen.
|
||||
tile.grove = Svamplund
|
||||
tile.grove_info = Ingen kan erövra den här mossan. Här växer jättesvampar upp igen.
|
||||
tile.tree = Träd
|
||||
tile.plant_info = Markera med hackan: en imp fäller den för {0} trä.
|
||||
tile.drawbridge = Vindbrygga
|
||||
@@ -724,6 +736,7 @@ net.error = {0}
|
||||
net.rooms = Öppna matcher
|
||||
net.no_rooms = Inga öppna matcher.
|
||||
net.create = Skapa match
|
||||
net.map_size = Karta {0}x{1} · {2} spelare
|
||||
net.coop_mission = Co-op-kampanj: uppdrag {0} ({1})
|
||||
net.coop_create = Co-op-kampanj
|
||||
net.coop_editor = Egen kampanj
|
||||
@@ -1083,12 +1096,20 @@ ability.drain.desc = läker sig själv med en tredjedel av skadan den gör.
|
||||
unit.giant = Jätte
|
||||
unit.fairy = Älvspion
|
||||
unit.battlemage = Stridsmagiker
|
||||
unit.thief = Tjuv
|
||||
unit.monk = Munk
|
||||
unit.ranger = Jägare
|
||||
unit.inquisitor = Inkvisitor
|
||||
unit.ram = Murbräcka
|
||||
unit.catapult = Katapult
|
||||
msg.trap_wrecked = Hjältarna förstörde en fälla: {0}.
|
||||
best.giant.text = En jätte är vad som händer när en riddare äter sina grönsaker i trehundra år. Långsam, enorm, och den ser dörrar som ett förslag. | Den förstör fällor bara genom att stå bredvid dem. Motbjudande manér. Slå den med allt, helst på avstånd.
|
||||
best.fairy.text = Ordens lilla angivare: en glödande mygga med vingar, utskickad i förväg för att hitta dina fällor och skvallra för alla. När den väl har sett en fälla slår hjältarna sönder den. | Den strör också läkande stoft över hjältarna. Smäll till den först. Två gånger, för säkerhets skull.
|
||||
best.battlemage.text = En magiker i ordens kåpa som ser väggar som en personlig förolämpning. Förstärkta väggar spricker under hans eldklot, och han slutar aldrig prata om det. | Ömtålig på nära håll. Stålväggar retar honom mest - bra.
|
||||
best.thief.text = Snabba fingrar, ännu snabbare fötter. Han kommer inte för att slåss: han smyger in i din skattkammare, fyller sin säck och springer tillbaka till porten. | Döda honom innan han kommer undan så är guldet ditt igen - med ränta.
|
||||
best.monk.text = År av fasta och bön har gjort honom döv för din magi: blixt, pest och frost rinner av honom som regn från ett tak. | Nävar gör också ont. Möt honom med stål, klor och tänder.
|
||||
best.ranger.text = En skogsman från orden som skjuter på avstånd och lägger ut järnfällor i dina gångar. En varelse som kliver i en blir skadad och fastnar en stund. | Fällorna ligger kvar tills någon kliver i dem. Hellre en kobold än din jätte.
|
||||
best.inquisitor.text = Skröplig, men hans predikningar är farliga: var tjugonde sekund övertalar han en av dina svagaste varelser - en kobold eller en spindel - att gå över till hjältarna. | Starka varelser bara skrattar åt honom. Slå till först.
|
||||
best.ram.text = En trädstam med järnhätta och ett tak mot pilar. Dina varelser struntar den i – dina dörrar, murar och torn inte. | Stoppa den innan den når dörren, eller skaffa en ny dörr.
|
||||
best.catapult.text = En träarm, en hink stenar och en riddare med för mycket fritid. Den når längre än dina torn och siktar på dem först. | Storma fram: på nära håll gör den inte en fluga förnär.
|
||||
|
||||
@@ -1272,6 +1293,7 @@ ed.terrain.ruby = Rubinåder
|
||||
ed.terrain.diamond = Diamantåder
|
||||
ed.terrain.steel = Stålvägg
|
||||
ed.terrain.mushroom = Jättesvamp
|
||||
ed.terrain.grove = Svamplund
|
||||
ed.terrain.grass = Gräs (ovan jord)
|
||||
ed.terrain.tree = Träd
|
||||
ed.place.core = Själskärna
|
||||
@@ -1354,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
|
||||
@@ -1373,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.
|
||||
@@ -1395,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
|
||||
@@ -1443,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).
|
||||
@@ -1673,3 +1701,70 @@ tut.slap = Crookfinger: Ha! Hörde du det? Något kommer från öster. Gör dig
|
||||
tut.obj6 = Besegra de två riddarna.
|
||||
tut.knights = Crookfinger: Två riddare! Skicka dina varelser med anfallsflaggan (Order) eller träffa dem med blixten.
|
||||
tut.win = Crookfinger: Bra gjort, mästare! Det är allt. Nåja – nästan. Kampanjen berättar resten. Jag sätter på tevattnet.
|
||||
|
||||
# master talents
|
||||
cat.master = Mästare
|
||||
talent.title = Mästartalanger
|
||||
talent.points = Lediga talangpoäng: {0}
|
||||
talent.next = {0}/{1} erfarenhet till nästa poäng
|
||||
talent.all = Alla talanger är inlärda.
|
||||
talent.builder = Byggmästare
|
||||
talent.warlord = Krigsherre
|
||||
talent.warlock = Häxmästare
|
||||
talent.merchant = Köpman
|
||||
talent.builder.desc = Hobar gräver {0}% snabbare, rum kostar {1}% mindre.
|
||||
talent.warlord.desc = Varelser slår {0}% hårdare och tar {1}% mindre skada.
|
||||
talent.warlock.desc = Mana flödar {0}% snabbare, besvärjelser kostar {1}% mindre.
|
||||
talent.merchant.desc = Lönerna är {0}% lägre, levererade skatter är värda {1}% mer.
|
||||
talent.none = Inte inlärd än.
|
||||
talent.then = Nästa nivå: {0}
|
||||
msg.talent_point = En ny talangpoäng! Använd den i mästarfönstret.
|
||||
msg.talent_learned = {0} nivå {1} inlärd.
|
||||
|
||||
# weather above ground
|
||||
weather.fair = Klart
|
||||
weather.rain = Regn
|
||||
weather.fog = Dimma
|
||||
weather.storm = Storm
|
||||
msg.weather_clear = Ovan jord klarnar det upp.
|
||||
msg.weather_rain = Ovan jord börjar det regna - eld tar inte fäste.
|
||||
msg.weather_fog = Dimma drar in över landet - ute ser alla sämre.
|
||||
msg.weather_storm = Ovan jord drar en storm in! Blixten slår ner i enheter och träd.
|
||||
msg.lightning_unit = Blixten träffade {0}!
|
||||
# creature names
|
||||
names.imp = Smula, Snorre, Grävis, Sörja, Ludd, Pyttan, Rotfrid, Snusk, Busen, Klösis, Smulan, Kladd, Gyttja, Pillis, Knölen, Spaden, Nosen, Pysen, Fläcken, Grums
|
||||
names.kobold = Glefs, Nyp, Griptand, Skabb, Hugg, Rostig, Svisch, Bitis, Gapis, Råttan, Trasa, Tagg, Klon, Bråk, Rask, Fniss, Knuffis, Hack, Knäck, Skråma
|
||||
names.spider = Krypis, Ullben, Spindeln, Kittla, Nätta, Åttafot, Tråden, Ryslig, Pillan, Silke, Väverskan, Hoppsan, Luddben, Tekla, Spolen, Nappa, Garnnystan, Klister, Vimsan, Nålen
|
||||
names.mage = Bartholomeus, Ignatius, Leopold, Theobald, Adalbert, Hieronymus, Valdemar, Gottfrid, Cornelius, Ottokar, Florentin, Engelbrekt, Pankratius, Rupertus, Konstantin, Eusebius, Severin, Barnabas, Archibald, Vitus
|
||||
names.ogre = Brumm, Kloss, Bums, Klump, Dunk, Tönt, Tjock, Klampen, Tramp, Träskalle, Plums, Mops, Bulle, Isterbuk, Klubba, Oxen, Rumla, Klabb, Hump, Gorbo
|
||||
names.jailer = Grym, Regel, Länken, Lås, Fängelse, Järnskägg, Bygel, Bult, Sträng, Galler, Grinig, Krok, Snara, Nyckel, Dyster, Klirr, Stockman, Väktare, Bur, Bitter
|
||||
names.priestess = Glöd, Flamma, Aska, Serafina, Låga, Eldine, Morwen, Brynhild, Ignia, Gnista, Scharlakan, Funka, Ravna, Lilith, Fnöske, Isolde, Brandhild, Nyx, Vesper, Glimt
|
||||
names.chainbreaker = Kedjebrytare, Ulfgar, Järnsjäl, Brannok, Kragmor, Valdrek, Torn, Gorath, Obunden, Rurik, Torvald, Hagen, Durnok, Splitter, Brand, Ulfrik, Korrun, Arvak, Sigurd, Halvard
|
||||
names.bat = Pip, Fladder, Pipsan, Susen, Eko, Flaxa, Skymning, Vissla, Kvitter, Tandis, Virvel, Fladdermus, Mal, Pipis, Susa, Dingla, Sammet, Smyg, Pipet, Blixten
|
||||
names.gargoyle = Granit, Skiffer, Basalt, Grimbert, Murbruk, Kisel, Grus, Flinta, Fog, Rasmassa, Kvarts, Pimpsten, Gesims, Knöl, Stenskägg, Sur, Gavel, Vattenspyare, Mejsel, Sockel
|
||||
names.owl = Uven, Viskis, Fjäderpenna, Bartholdus, Minerva, Tobias, Huhu, Ugglalia, Fjäderik, Suck, Grubblis, Äldste, Kattuggla, Noctua, Ugglebert, Pergament, Skymning, Nattuggla, Professorn, Dun
|
||||
names.wyrm = Vermithrax, Askgap, Sveda, Calderon, Ignivar, Glödhorn, Drakkar, Pyrrhus, Obsidian, Glödgap, Vulkanor, Brandtand, Malgrath, Tyrax, Inferno, Svavel, Kaldor, Fafner, Ragnor, Sinderos
|
||||
names.cogling = Kugghjul, Hjulet, Skruven, Spärrhake, Bult, Växel, Fixaren, Grejen, Nyckeln, Drev, Klirra, Pillaren, Nita, Snurra, Vev, Snurrebass, Fläns, Klickklack, Ticktack, Nippel
|
||||
names.revenant = Morgon, Agg, Askgrå, Klagan, Ihålig, Sorg, Blek, Kummer, Skugga, Mylla, Gravman, Skymningen, Krypta, Vemod, Vaka, Hög, Liksvepning, Elegi, Drägg, Öde
|
||||
names.beast = Lurv, Brummbjörn, Bruno, Raggis, Mumsa, Mossrygg, Brummer, Ursa, Knorr, Knaster, Klickare, Krypare, Borst, Mossa, Mullret, Törnsnår, Knirr, Gnällis, Nalle, Nosis
|
||||
names.human = Gerhard, Roland, Konrad, Albrekt, Tristan, Eskil, Gavin, Lancelot, Pål, Håkan, Bertil, Gottfrid, Erik, Sverker, Ragnar, Vilfred, Ditlev, Gilbert, Rasmus, Birger
|
||||
epithets.mid = Benkrossare, Gyttjefot, Råttbitare, Smutsfejs, Stengnagare, Skallbankare, Stinknäve, Rostknoge, Magboxare, Smalbensspark, Mögelskägg, Smutsklo, Snorslunga, Örongnagare, Knäknäckare, Maskätare
|
||||
epithets.high = Kungadråpare, Ödeshand, Skräcktand, Världskrossare, Hjältebane, Stormnäve, Nattskräck, Själaskördare, Järnhjärta, Borgkrossare, Drakdräpare, Helvetesvandrare, Skuggfurste, Blodkrona, Dödsflod, Stjärnkrossare
|
||||
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;
|
||||
}
|
||||
|
||||
+69
-32
@@ -15,40 +15,40 @@ pub const STAT_NAMES: [&str; k::COUNTERS] = [
|
||||
"heroes", "bosses", "lost", "dug", "wood", "walls", "rooms", "spells", "slaps", "traps", "towers", "drowned", "burnt", "siege", "captured", "gold", "drawbridge",
|
||||
];
|
||||
|
||||
// tiered achievements: id (= counter), thresholds, icon cell
|
||||
// tiered achievements: id (= counter), thresholds, badge (badges.rs)
|
||||
pub const TIERS: [(&str, &[f64], u32); 20] = [
|
||||
("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::gfx::I_ARROW),
|
||||
("bosses", &[1.0, 5.0, 20.0], crate::gfx::I_RUNE),
|
||||
("dug", &[100.0, 1000.0, 10000.0], crate::gfx::I_PICK),
|
||||
("gold", &[10000.0, 100000.0, 1000000.0], crate::gfx::I_SELL),
|
||||
("wood", &[50.0, 500.0, 2000.0], crate::gfx::I_BRIDGE),
|
||||
("walls", &[10.0, 100.0], crate::gfx::I_WALL),
|
||||
("rooms", &[50.0, 500.0, 2000.0], crate::gfx::I_STEEL),
|
||||
("spells", &[10.0, 100.0, 500.0], crate::gfx::I_BOLT),
|
||||
("slaps", &[10.0, 100.0, 1000.0], crate::gfx::I_HAND),
|
||||
("traps", &[5.0, 50.0, 200.0], crate::gfx::I_TRAPS),
|
||||
("towers", &[5.0, 50.0], crate::gfx::I_CANNON),
|
||||
("drowned", &[1.0, 25.0, 100.0], crate::gfx::I_FREEZE),
|
||||
("burnt", &[5.0, 50.0], crate::gfx::I_DRAWBRIDGE),
|
||||
("siege", &[1.0, 10.0], crate::gfx::I_CATAPULT),
|
||||
("captured", &[1.0, 25.0, 100.0], crate::gfx::I_SUMMON),
|
||||
("drawbridge", &[1.0, 50.0], crate::gfx::I_DRAWBRIDGE),
|
||||
("wins", &[1.0, 10.0, 50.0], crate::gfx::I_FLAG),
|
||||
("missions", &[1.0, 10.0, 30.0], crate::gfx::I_MAGIC),
|
||||
("coop", &[1.0, 10.0, 20.0], crate::gfx::I_HEAL),
|
||||
("minutes", &[60.0, 600.0, 3000.0], crate::gfx::I_SPELLS),
|
||||
("heroes", &[1.0, 50.0, 250.0, 1000.0], crate::badges::HEROES),
|
||||
("bosses", &[1.0, 5.0, 20.0], crate::badges::BOSSES),
|
||||
("dug", &[100.0, 1000.0, 10000.0], crate::badges::DUG),
|
||||
("gold", &[10000.0, 100000.0, 1000000.0], crate::badges::GOLD),
|
||||
("wood", &[50.0, 500.0, 2000.0], crate::badges::WOOD),
|
||||
("walls", &[10.0, 100.0], crate::badges::WALLS),
|
||||
("rooms", &[50.0, 500.0, 2000.0], crate::badges::ROOMS),
|
||||
("spells", &[10.0, 100.0, 500.0], crate::badges::SPELLS),
|
||||
("slaps", &[10.0, 100.0, 1000.0], crate::badges::SLAPS),
|
||||
("traps", &[5.0, 50.0, 200.0], crate::badges::TRAPS),
|
||||
("towers", &[5.0, 50.0], crate::badges::TOWERS),
|
||||
("drowned", &[1.0, 25.0, 100.0], crate::badges::DROWNED),
|
||||
("burnt", &[5.0, 50.0], crate::badges::BURNT),
|
||||
("siege", &[1.0, 10.0], crate::badges::SIEGE),
|
||||
("captured", &[1.0, 25.0, 100.0], crate::badges::CAPTURED),
|
||||
("drawbridge", &[1.0, 50.0], crate::badges::DRAWBRIDGE),
|
||||
("wins", &[1.0, 10.0, 50.0], crate::badges::WINS),
|
||||
("missions", &[1.0, 10.0, 30.0], crate::badges::MISSIONS),
|
||||
("coop", &[1.0, 10.0, 20.0], crate::badges::COOP),
|
||||
("minutes", &[60.0, 600.0, 3000.0], crate::badges::MINUTES),
|
||||
];
|
||||
|
||||
// special achievements: id, hidden until unlocked, icon cell
|
||||
// special achievements: id, hidden until unlocked, badge (badges.rs)
|
||||
pub const SPECIAL: [(&str, bool, u32); 8] = [
|
||||
("flawless", false, crate::gfx::I_HEAL),
|
||||
("speedrun", false, crate::gfx::I_BOLT),
|
||||
("campaign", false, crate::gfx::I_RUNE),
|
||||
("surface", false, crate::gfx::I_WALL),
|
||||
("defeat", true, crate::gfx::I_SELL),
|
||||
("legend", false, crate::gfx::I_ARROW),
|
||||
("wyrm", true, crate::gfx::I_SUMMON),
|
||||
("fullhand", true, crate::gfx::I_HAND),
|
||||
("flawless", false, crate::badges::FLAWLESS),
|
||||
("speedrun", false, crate::badges::SPEEDRUN),
|
||||
("campaign", false, crate::badges::CAMPAIGN),
|
||||
("surface", false, crate::badges::SURFACE),
|
||||
("defeat", true, crate::badges::DEFEAT),
|
||||
("legend", false, crate::badges::LEGEND),
|
||||
("wyrm", true, crate::badges::WYRM),
|
||||
("fullhand", true, crate::badges::FULLHAND),
|
||||
];
|
||||
|
||||
// number of achievements in all
|
||||
@@ -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);
|
||||
@@ -244,7 +271,7 @@ pub fn fmt_count(v: f64) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
// every achievement in display order: (id, hidden, icon, tier 0..)
|
||||
// every achievement in display order: (id, hidden, badge, tier 0..)
|
||||
pub fn all() -> Vec<(String, bool, u32, usize)> {
|
||||
let mut v = Vec::new();
|
||||
for (id, steps, icon) in TIERS.iter() {
|
||||
@@ -268,6 +295,16 @@ mod tests {
|
||||
assert_eq!(all().len(), count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_achievement_has_its_own_badge() {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for b in TIERS.iter().map(|t| t.2).chain(SPECIAL.iter().map(|s| s.2)) {
|
||||
assert!(b < crate::badges::COUNT, "badge {} out of range", b);
|
||||
assert!(seen.insert(b), "badge {} used twice", b);
|
||||
}
|
||||
assert_eq!(seen.len() as u32, crate::badges::COUNT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tracker_counts_a_game_and_unlocks() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
|
||||
@@ -10,11 +10,13 @@ use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
// per difficulty (easy, medium, hard)
|
||||
const THINK: [f32; 3] = [4.0, 2.5, 1.5]; // seconds between two thoughts
|
||||
const THINK: [f32; 3] = [5.2, 3.25, 1.95]; // seconds between two thoughts (a calm pace)
|
||||
const MAX_RADIUS: [i32; 3] = [11, 15, 19]; // how far the dungeon grows around the heart
|
||||
const MAX_IMPS: [usize; 3] = [6, 9, 12];
|
||||
const FIRST_ATTACK: [f32; 3] = [720.0, 480.0, 330.0]; // no attack before (seconds)
|
||||
const BOLDNESS: [f32; 3] = [1.5, 1.15, 0.9]; // own army / enemy army needed to attack
|
||||
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.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];
|
||||
|
||||
@@ -50,16 +52,63 @@ 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);
|
||||
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
|
||||
fn ai_reach_mask(&self) -> Vec<bool> {
|
||||
let me = self.me;
|
||||
let mut dist = vec![u8::MAX; self.map.t.len()];
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
for (i, t) in self.map.t.iter().enumerate() {
|
||||
if t.owner == me {
|
||||
dist[i] = 0;
|
||||
queue.push_back(i);
|
||||
}
|
||||
}
|
||||
while let Some(i) = queue.pop_front() {
|
||||
let d = dist[i];
|
||||
if d as i32 >= MARK_REACH {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = xy(i);
|
||||
for &(dx, dy) in DIR8.iter() {
|
||||
if inb(x + dx, y + dy) {
|
||||
let j = idx(x + dx, y + dy);
|
||||
if dist[j] > d + 1 {
|
||||
dist[j] = d + 1;
|
||||
queue.push_back(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dist.into_iter().map(|d| d as i32 <= MARK_REACH).collect()
|
||||
}
|
||||
|
||||
// marks a tile for this master's imps (only near its own ground)
|
||||
fn ai_mark(&mut self, i: usize, mask: &[bool]) -> bool {
|
||||
if !mask.get(i).copied().unwrap_or(false) {
|
||||
return false;
|
||||
}
|
||||
let side = self.me.side().min(7);
|
||||
let t = &mut self.map.t[i];
|
||||
if t.marked && t.mark_by != side {
|
||||
return false; // another master's plan
|
||||
}
|
||||
t.marked = true;
|
||||
t.mark_by = side;
|
||||
true
|
||||
}
|
||||
|
||||
// enough imps for the dungeon's size
|
||||
fn ai_imps(&mut self, d: usize, cx: i32, cy: i32) {
|
||||
let claimed = self.map.t.iter().filter(|t| t.owner == self.me).count();
|
||||
let want = (4 + claimed / 30).min(MAX_IMPS[d]);
|
||||
if self.worker_count() < want && self.mana >= self.summon_cost() + 20.0 {
|
||||
if self.worker_count() < want && self.mana >= self.summon_cost() {
|
||||
self.summon(cx as f32 + 0.5, cy as f32 + 2.5);
|
||||
}
|
||||
}
|
||||
@@ -67,12 +116,13 @@ impl Game {
|
||||
// dig: rings of earth around the heart, veins first; the ring grows when it is dug out
|
||||
fn ai_dig(&mut self, k: usize, d: usize, cx: i32, cy: i32) {
|
||||
let me = self.me;
|
||||
let side = me.side().min(7);
|
||||
let r = self.rivals[k].radius.max(5);
|
||||
let marked_mine = |g: &Game| {
|
||||
let mut n = 0;
|
||||
for y in cy - r - 4..=cy + r + 4 {
|
||||
for x in cx - r - 4..=cx + r + 4 {
|
||||
if g.map.get(x, y).map_or(false, |t| t.marked) {
|
||||
if g.map.get(x, y).map_or(false, |t| t.marked && t.mark_by == side) {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
@@ -84,13 +134,14 @@ impl Game {
|
||||
return; // the imps have enough to do
|
||||
}
|
||||
let mut added = 0;
|
||||
let mask = self.ai_reach_mask();
|
||||
let diggable = |t: &Tile| t.diggable() && !t.plant() && !t.reinforced && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me);
|
||||
// veins a little beyond the ring, then the ring itself
|
||||
for pass in 0..2 {
|
||||
let reach = if pass == 0 { r + 3 } else { r };
|
||||
for y in cy - reach..=cy + reach {
|
||||
for x in cx - reach..=cx + reach {
|
||||
if added >= 16 || !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
if added >= 11 || !inb(x, y) || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
||||
continue;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
@@ -98,12 +149,12 @@ impl Game {
|
||||
if t.marked || !diggable(&t) || (pass == 0 && t.vein().is_none()) {
|
||||
continue;
|
||||
}
|
||||
self.map.t[i].marked = true;
|
||||
self.map.t[i].mark_by = self.me.side().min(7);
|
||||
if self.ai_mark(i, &mask) {
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// the ring is done: grow
|
||||
if added == 0 && open == 0 && r < MAX_RADIUS[d] {
|
||||
self.rivals[k].radius = r + 2;
|
||||
@@ -112,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];
|
||||
@@ -146,12 +202,11 @@ impl Game {
|
||||
};
|
||||
let Some(path) = found else { return false };
|
||||
let me = self.me;
|
||||
let mask = self.ai_reach_mask();
|
||||
let mut cluster = Vec::new();
|
||||
for &i in &path {
|
||||
let t = self.map.t[i];
|
||||
if t.diggable() && !t.plant() && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me || t.reinforced) {
|
||||
self.map.t[i].marked = true;
|
||||
self.map.t[i].mark_by = self.me.side().min(7);
|
||||
if t.diggable() && !t.plant() && t.kind != Kind::Steel && (t.owner == Owner::None || t.owner == me || t.reinforced) && self.ai_mark(i, &mask) {
|
||||
if t.vein().is_some() {
|
||||
cluster.push(i);
|
||||
}
|
||||
@@ -163,8 +218,7 @@ impl Game {
|
||||
for &(dx, dy) in DIR8.iter() {
|
||||
if let Some(t) = self.map.get(x + dx, y + dy) {
|
||||
if t.vein().is_some() && t.owner == Owner::None {
|
||||
self.map.t[idx(x + dx, y + dy)].marked = true;
|
||||
self.map.t[idx(x + dx, y + dy)].mark_by = self.me.side().min(7);
|
||||
self.ai_mark(idx(x + dx, y + dy), &mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,8 +227,12 @@ 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)) {
|
||||
continue;
|
||||
}
|
||||
let tiles = self.counts[room];
|
||||
if tiles >= (w * h) as usize * n {
|
||||
continue;
|
||||
@@ -188,6 +246,34 @@ 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)?
|
||||
fn ai_knows(&self, tool: crate::ui::Tool) -> bool {
|
||||
crate::ui::TOOLS.iter().position(|&t| t == tool).map_or(false, |i| self.tool_allowed(i))
|
||||
}
|
||||
|
||||
// a rectangle of own plain floor, as near the heart as possible (not right next to it)
|
||||
@@ -214,7 +300,7 @@ impl Game {
|
||||
fn ai_towers(&mut self, d: usize, cx: i32, cy: i32) {
|
||||
let me = self.me;
|
||||
let mine = self.towers.iter().filter(|t| !t.hero && self.map.t[t.tile].owner == me).count();
|
||||
if mine >= MAX_TOWERS[d] || self.gold < 1800.0 || self.counts[0] == 0 {
|
||||
if mine >= MAX_TOWERS[d] || self.gold < 1800.0 || self.counts[0] == 0 || !self.ai_knows(crate::ui::Tool::Tower(TowerKind::Arrow)) {
|
||||
return;
|
||||
}
|
||||
let spot = (0..self.map.t.len()).filter(|&i| {
|
||||
@@ -239,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));
|
||||
@@ -307,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::*;
|
||||
|
||||
+929
@@ -0,0 +1,929 @@
|
||||
// Achievement badges: one 32x32 pixel-art symbol per achievement, generated at start into a
|
||||
// texture of its own (the main atlas is full). Each symbol is painted from simple shapes in
|
||||
// materials (steel, gold, wood, ...); every material has a ramp of four shades, and the shapes
|
||||
// are bevelled automatically (lit from the top left) and get a dark outline.
|
||||
|
||||
use macroquad::prelude::*;
|
||||
|
||||
pub const SIZE: u32 = 32;
|
||||
const COLS: u32 = 8;
|
||||
|
||||
// badge indices (the icon field of achievements::TIERS / SPECIAL)
|
||||
pub const HEROES: u32 = 0;
|
||||
pub const BOSSES: u32 = 1;
|
||||
pub const DUG: u32 = 2;
|
||||
pub const GOLD: u32 = 3;
|
||||
pub const WOOD: u32 = 4;
|
||||
pub const WALLS: u32 = 5;
|
||||
pub const ROOMS: u32 = 6;
|
||||
pub const SPELLS: u32 = 7;
|
||||
pub const SLAPS: u32 = 8;
|
||||
pub const TRAPS: u32 = 9;
|
||||
pub const TOWERS: u32 = 10;
|
||||
pub const DROWNED: u32 = 11;
|
||||
pub const BURNT: u32 = 12;
|
||||
pub const SIEGE: u32 = 13;
|
||||
pub const CAPTURED: u32 = 14;
|
||||
pub const DRAWBRIDGE: u32 = 15;
|
||||
pub const WINS: u32 = 16;
|
||||
pub const MISSIONS: u32 = 17;
|
||||
pub const COOP: u32 = 18;
|
||||
pub const MINUTES: u32 = 19;
|
||||
pub const FLAWLESS: u32 = 20;
|
||||
pub const SPEEDRUN: u32 = 21;
|
||||
pub const CAMPAIGN: u32 = 22;
|
||||
pub const SURFACE: u32 = 23;
|
||||
pub const DEFEAT: u32 = 24;
|
||||
pub const LEGEND: u32 = 25;
|
||||
pub const WYRM: u32 = 26;
|
||||
pub const FULLHAND: u32 = 27;
|
||||
pub const COUNT: u32 = 28;
|
||||
|
||||
// the source rectangle of a badge in the badge texture
|
||||
pub fn badge_rect(i: u32) -> Rect {
|
||||
Rect::new(((i % COLS) * SIZE) as f32, ((i / COLS) * SIZE) as f32, SIZE as f32, SIZE as f32)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ materials
|
||||
|
||||
const EMPTY: u8 = 0;
|
||||
const STEEL: u8 = 1;
|
||||
const GOLDM: u8 = 2;
|
||||
const WOODM: u8 = 3;
|
||||
const STONE: u8 = 4;
|
||||
const RED: u8 = 5;
|
||||
const FIRE: u8 = 6;
|
||||
const WATER: u8 = 7;
|
||||
const CRYSTAL: u8 = 8;
|
||||
const GREEN: u8 = 9;
|
||||
const BONE: u8 = 10;
|
||||
const PURPLE: u8 = 11;
|
||||
const IRON: u8 = 12; // dark iron, also slits and holes
|
||||
const BRICK: u8 = 13;
|
||||
const SKIN: u8 = 14; // imp green (the keeper's hand)
|
||||
const DKWOOD: u8 = 15;
|
||||
const GLASS: u8 = 16;
|
||||
const CYAN: u8 = 17; // magic glow
|
||||
const SUN: u8 = 18;
|
||||
const BLUE: u8 = 19;
|
||||
const SPARK: u8 = 20; // white sparkles and motion lines, no outline
|
||||
const EMBER: u8 = 21; // floating embers and steam, no outline
|
||||
const TEA: u8 = 22;
|
||||
|
||||
fn ramp(m: u8) -> [(u8, u8, u8); 4] {
|
||||
match m {
|
||||
STEEL => [(66, 72, 90), (116, 124, 144), (166, 174, 190), (222, 228, 238)],
|
||||
GOLDM => [(124, 76, 22), (196, 138, 38), (238, 192, 70), (255, 240, 150)],
|
||||
WOODM => [(78, 46, 26), (120, 76, 42), (156, 106, 62), (196, 146, 92)],
|
||||
STONE => [(58, 54, 64), (90, 84, 96), (124, 118, 130), (160, 154, 166)],
|
||||
RED => [(96, 18, 24), (156, 34, 36), (204, 62, 50), (240, 122, 92)],
|
||||
FIRE => [(196, 54, 18), (240, 124, 30), (255, 192, 60), (255, 244, 170)],
|
||||
WATER => [(22, 44, 96), (38, 84, 148), (66, 134, 198), (150, 200, 240)],
|
||||
CRYSTAL => [(60, 34, 120), (110, 66, 196), (164, 120, 244), (226, 206, 255)],
|
||||
GREEN => [(34, 66, 30), (58, 108, 42), (98, 150, 58), (152, 202, 92)],
|
||||
BONE => [(120, 104, 84), (178, 160, 128), (216, 202, 170), (244, 236, 214)],
|
||||
PURPLE => [(50, 26, 60), (88, 42, 100), (128, 68, 140), (176, 110, 182)],
|
||||
IRON => [(22, 18, 24), (40, 34, 42), (62, 54, 64), (88, 80, 90)],
|
||||
BRICK => [(88, 36, 30), (134, 60, 44), (170, 88, 62), (204, 126, 90)],
|
||||
SKIN => [(56, 84, 44), (98, 138, 72), (140, 184, 102), (184, 222, 140)],
|
||||
DKWOOD => [(50, 30, 20), (78, 48, 30), (104, 68, 42), (134, 94, 60)],
|
||||
GLASS => [(92, 128, 150), (136, 176, 196), (180, 214, 228), (232, 246, 252)],
|
||||
CYAN => [(30, 110, 150), (60, 180, 220), (130, 230, 250), (220, 255, 255)],
|
||||
SUN => [(220, 120, 30), (250, 180, 50), (255, 222, 90), (255, 250, 200)],
|
||||
BLUE => [(24, 30, 80), (40, 52, 128), (66, 84, 172), (110, 132, 214)],
|
||||
SPARK => [(200, 200, 210), (236, 236, 244), (255, 255, 255), (255, 255, 255)],
|
||||
EMBER => [(200, 80, 30), (255, 150, 50), (255, 210, 110), (255, 250, 220)],
|
||||
TEA => [(70, 36, 20), (104, 58, 30), (134, 82, 44), (170, 112, 66)],
|
||||
_ => [(0, 0, 0); 4],
|
||||
}
|
||||
}
|
||||
|
||||
fn outlined(m: u8) -> bool {
|
||||
m != EMPTY && m != SPARK && m != EMBER
|
||||
}
|
||||
|
||||
const OUTLINE: (u8, u8, u8) = (18, 12, 16);
|
||||
const AUTO: u8 = 255;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct P {
|
||||
m: u8,
|
||||
s: u8, // shade 0..3, or AUTO (bevelled)
|
||||
}
|
||||
|
||||
fn a(m: u8) -> P {
|
||||
P { m, s: AUTO }
|
||||
}
|
||||
fn f(m: u8, s: u8) -> P {
|
||||
P { m, s }
|
||||
}
|
||||
const CUT: P = P { m: EMPTY, s: AUTO };
|
||||
|
||||
// one badge being painted
|
||||
struct Pad {
|
||||
m: [u8; 1024],
|
||||
s: [u8; 1024],
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
fn new() -> Pad {
|
||||
Pad { m: [EMPTY; 1024], s: [AUTO; 1024] }
|
||||
}
|
||||
|
||||
fn px(&mut self, x: i32, y: i32, p: P) {
|
||||
if (0..32).contains(&x) && (0..32).contains(&y) {
|
||||
let i = (y * 32 + x) as usize;
|
||||
self.m[i] = p.m;
|
||||
self.s[i] = p.s;
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, x: i32, y: i32) -> u8 {
|
||||
if (0..32).contains(&x) && (0..32).contains(&y) {
|
||||
self.m[(y * 32 + x) as usize]
|
||||
} else {
|
||||
EMPTY
|
||||
}
|
||||
}
|
||||
|
||||
// every pixel whose centre passes the test
|
||||
fn fill(&mut self, p: P, test: impl Fn(f32, f32) -> bool) {
|
||||
for y in 0..32 {
|
||||
for x in 0..32 {
|
||||
if test(x as f32 + 0.5, y as f32 + 0.5) {
|
||||
self.px(x, y, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inclusive pixel rectangle
|
||||
fn rect(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, p: P) {
|
||||
for y in y0..=y1 {
|
||||
for x in x0..=x1 {
|
||||
self.px(x, y, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disc(&mut self, cx: f32, cy: f32, r: f32, p: P) {
|
||||
self.fill(p, |x, y| (x - cx).powi(2) + (y - cy).powi(2) <= r * r);
|
||||
}
|
||||
|
||||
fn ring(&mut self, cx: f32, cy: f32, r0: f32, r1: f32, p: P) {
|
||||
self.fill(p, |x, y| {
|
||||
let d = (x - cx).powi(2) + (y - cy).powi(2);
|
||||
d <= r1 * r1 && d >= r0 * r0
|
||||
});
|
||||
}
|
||||
|
||||
// ellipse turned by `ang` (radians)
|
||||
fn oval(&mut self, cx: f32, cy: f32, rx: f32, ry: f32, ang: f32, p: P) {
|
||||
let (s, c) = ang.sin_cos();
|
||||
self.fill(p, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let u = dx * c + dy * s;
|
||||
let v = -dx * s + dy * c;
|
||||
(u / rx).powi(2) + (v / ry).powi(2) <= 1.0
|
||||
});
|
||||
}
|
||||
|
||||
// a thick line (round ends)
|
||||
fn line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, w: f32, p: P) {
|
||||
let (dx, dy) = (x1 - x0, y1 - y0);
|
||||
let len2 = (dx * dx + dy * dy).max(1e-6);
|
||||
let r = w * 0.5;
|
||||
self.fill(p, |x, y| {
|
||||
let t = (((x - x0) * dx + (y - y0) * dy) / len2).clamp(0.0, 1.0);
|
||||
let (qx, qy) = (x0 + t * dx - x, y0 + t * dy - y);
|
||||
qx * qx + qy * qy <= r * r
|
||||
});
|
||||
}
|
||||
|
||||
// a line through several points
|
||||
fn path(&mut self, pts: &[(f32, f32)], w: f32, p: P) {
|
||||
for s in pts.windows(2) {
|
||||
self.line(s[0].0, s[0].1, s[1].0, s[1].1, w, p);
|
||||
}
|
||||
}
|
||||
|
||||
fn poly(&mut self, pts: &[(f32, f32)], p: P) {
|
||||
self.fill(p, |x, y| {
|
||||
let mut inside = false;
|
||||
let n = pts.len();
|
||||
let mut j = n - 1;
|
||||
for i in 0..n {
|
||||
let (xi, yi) = pts[i];
|
||||
let (xj, yj) = pts[j];
|
||||
if (yi > y) != (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
inside
|
||||
});
|
||||
}
|
||||
|
||||
// a five-pointed star
|
||||
fn star(&mut self, cx: f32, cy: f32, r0: f32, r1: f32, p: P) {
|
||||
let pts: Vec<(f32, f32)> = (0..10)
|
||||
.map(|k| {
|
||||
let ang = -std::f32::consts::FRAC_PI_2 + k as f32 * std::f32::consts::PI / 5.0;
|
||||
let r = if k % 2 == 0 { r1 } else { r0 };
|
||||
(cx + r * ang.cos(), cy + r * ang.sin())
|
||||
})
|
||||
.collect();
|
||||
self.poly(&pts, p);
|
||||
}
|
||||
|
||||
// a small four-pointed sparkle
|
||||
fn sparkle(&mut self, x: i32, y: i32) {
|
||||
let p = f(SPARK, 2);
|
||||
self.px(x, y, p);
|
||||
for (dx, dy) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
|
||||
self.px(x + dx, y + dy, f(SPARK, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// bevel, outline and write into the image at cell `i`
|
||||
fn blit(&self, img: &mut Image, i: u32) {
|
||||
let (ox, oy) = ((i % COLS) * SIZE, (i / COLS) * SIZE);
|
||||
for y in 0..32i32 {
|
||||
for x in 0..32i32 {
|
||||
let m = self.get(x, y);
|
||||
let col = if m == EMPTY {
|
||||
let near = [(1, 0), (-1, 0), (0, 1), (0, -1)].iter().any(|(dx, dy)| outlined(self.get(x + dx, y + dy)));
|
||||
if !near {
|
||||
continue;
|
||||
}
|
||||
OUTLINE
|
||||
} else {
|
||||
let mut s = self.s[(y * 32 + x) as usize];
|
||||
if s == AUTO {
|
||||
let lit = self.get(x - 1, y) != m || self.get(x, y - 1) != m;
|
||||
let dark = self.get(x + 1, y) != m || self.get(x, y + 1) != m;
|
||||
s = match (lit, dark) {
|
||||
(true, false) => 2,
|
||||
(false, true) => 0,
|
||||
_ => 1,
|
||||
};
|
||||
// the top-left corner catches the light
|
||||
if lit && !dark && self.get(x - 1, y) != m && self.get(x, y - 1) != m {
|
||||
s = 3;
|
||||
}
|
||||
}
|
||||
ramp(m)[s.min(3) as usize]
|
||||
};
|
||||
img.set_pixel(ox + x as u32, oy + y as u32, Color::from_rgba(col.0, col.1, col.2, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the symbols
|
||||
|
||||
fn heroes(p: &mut Pad) {
|
||||
// a knight's great helm, split by a blow
|
||||
p.poly(&[(16.0, 1.5), (21.0, 4.0), (19.0, 7.0), (16.0, 6.0)], a(RED)); // plume
|
||||
p.disc(16.0, 13.0, 8.5, a(STEEL));
|
||||
p.rect(8, 13, 24, 25, a(STEEL));
|
||||
p.rect(9, 14, 23, 15, f(IRON, 0)); // visor slit
|
||||
p.line(16.0, 6.0, 16.0, 25.0, 1.0, f(STEEL, 0)); // ridge
|
||||
for (x, y) in [(11, 19), (13, 21), (11, 22), (20, 19), (22, 21)] {
|
||||
p.px(x, y, f(IRON, 1));
|
||||
}
|
||||
// the crack and the broken-off piece
|
||||
p.poly(&[(19.0, 3.0), (26.0, 3.0), (26.0, 12.0), (21.0, 9.0)], CUT);
|
||||
p.path(&[(21.0, 9.0), (18.0, 13.0), (20.0, 17.0), (17.5, 22.0)], 1.0, f(IRON, 0));
|
||||
p.poly(&[(23.0, 26.0), (28.0, 24.0), (29.5, 29.5), (24.0, 29.5)], a(STEEL));
|
||||
p.line(24.0, 26.5, 28.0, 25.0, 0.8, f(STEEL, 3));
|
||||
p.px(27, 7, f(STEEL, 2));
|
||||
p.px(29, 11, f(STEEL, 1));
|
||||
p.sparkle(28, 18);
|
||||
}
|
||||
|
||||
fn bosses(p: &mut Pad) {
|
||||
// a skull wearing a cracked crown
|
||||
p.disc(16.0, 19.0, 9.0, a(BONE));
|
||||
p.rect(11, 24, 21, 29, a(BONE));
|
||||
p.disc(12.0, 19.0, 2.6, f(IRON, 0));
|
||||
p.disc(20.0, 19.0, 2.6, f(IRON, 0));
|
||||
p.px(12, 19, f(RED, 3));
|
||||
p.px(20, 19, f(RED, 3));
|
||||
p.poly(&[(16.0, 21.5), (14.5, 24.5), (17.5, 24.5)], f(IRON, 0));
|
||||
for x in [12, 14, 16, 18, 20] {
|
||||
p.line(x as f32 + 0.5, 26.0, x as f32 + 0.5, 29.0, 0.8, f(BONE, 0));
|
||||
}
|
||||
p.poly(&[(6.0, 13.0), (6.0, 3.0), (10.5, 8.0), (16.0, 1.0), (21.5, 8.0), (26.0, 3.0), (26.0, 13.0)], a(GOLDM));
|
||||
p.rect(6, 11, 25, 13, f(GOLDM, 0));
|
||||
p.disc(16.0, 8.5, 1.8, a(RED));
|
||||
p.disc(9.0, 10.0, 1.2, f(CRYSTAL, 2));
|
||||
p.disc(23.0, 10.0, 1.2, f(CRYSTAL, 2));
|
||||
p.path(&[(13.0, 3.0), (14.5, 7.0), (12.5, 10.0), (13.5, 13.5)], 1.2, CUT);
|
||||
}
|
||||
|
||||
fn dug(p: &mut Pad) {
|
||||
// a pickaxe struck into a rock
|
||||
p.poly(&[(2.0, 30.0), (4.0, 21.0), (10.0, 16.0), (18.0, 17.0), (25.0, 20.0), (30.0, 30.0)], a(STONE));
|
||||
p.path(&[(8.0, 22.0), (12.0, 25.0), (11.0, 29.0)], 1.0, f(STONE, 0));
|
||||
p.path(&[(21.0, 23.0), (24.0, 27.0)], 1.0, f(STONE, 0));
|
||||
p.rect(5, 26, 6, 27, a(GOLDM));
|
||||
p.rect(16, 27, 17, 28, a(GOLDM));
|
||||
p.px(26, 26, f(GOLDM, 2));
|
||||
p.line(25.0, 5.0, 13.0, 23.0, 2.6, a(WOODM));
|
||||
p.path(&[(10.0, 7.0), (16.0, 3.0), (23.0, 4.0)], 3.2, a(STEEL));
|
||||
p.path(&[(23.0, 4.0), (28.0, 9.0), (29.0, 15.0)], 3.0, a(STEEL));
|
||||
p.line(9.0, 7.5, 10.0, 7.0, 1.6, a(STEEL));
|
||||
p.disc(24.0, 5.5, 2.4, a(IRON));
|
||||
// chips flying
|
||||
p.rect(5, 12, 6, 13, a(STONE));
|
||||
p.rect(8, 9, 8, 10, a(STONE));
|
||||
p.px(3, 16, f(STONE, 2));
|
||||
}
|
||||
|
||||
fn gold(p: &mut Pad) {
|
||||
// a pile of coins
|
||||
p.poly(&[(3.0, 29.0), (7.0, 21.0), (13.0, 17.0), (20.0, 17.0), (26.0, 21.0), (29.0, 29.0)], a(GOLDM));
|
||||
for (x, y) in [(8, 25), (14, 22), (20, 25), (24, 23), (11, 27), (17, 27)] {
|
||||
p.oval(x as f32 + 0.5, y as f32, 2.6, 1.2, 0.0, f(GOLDM, 0));
|
||||
p.oval(x as f32 + 0.5, y as f32 - 0.5, 2.0, 0.8, 0.0, f(GOLDM, 2));
|
||||
}
|
||||
// a standing coin
|
||||
p.disc(12.0, 13.0, 7.0, a(GOLDM));
|
||||
p.ring(12.0, 13.0, 4.6, 5.6, f(GOLDM, 0));
|
||||
p.rect(11, 10, 12, 16, f(GOLDM, 0));
|
||||
p.rect(10, 10, 13, 10, f(GOLDM, 0));
|
||||
p.rect(10, 16, 13, 16, f(GOLDM, 0));
|
||||
// another coin flying
|
||||
p.oval(24.0, 9.0, 4.0, 4.0, 0.0, a(GOLDM));
|
||||
p.ring(24.0, 9.0, 2.0, 2.9, f(GOLDM, 0));
|
||||
p.sparkle(27, 16);
|
||||
p.sparkle(5, 7);
|
||||
}
|
||||
|
||||
fn wood(p: &mut Pad) {
|
||||
// an axe buried in a log
|
||||
p.rect(3, 18, 23, 27, a(WOODM));
|
||||
for (x0, x1, y) in [(5, 12, 20), (9, 19, 23), (4, 9, 25), (14, 21, 26)] {
|
||||
p.rect(x0, y, x1, y, f(WOODM, 0));
|
||||
}
|
||||
p.oval(24.5, 22.5, 4.0, 5.2, 0.0, f(BONE, 1));
|
||||
p.ring(24.5, 22.5, 1.5, 2.3, f(WOODM, 2));
|
||||
p.ring(24.5, 22.5, 3.2, 3.9, f(WOODM, 2));
|
||||
p.px(24, 22, f(WOODM, 1));
|
||||
p.line(5.0, 3.0, 15.0, 17.0, 2.6, a(DKWOOD));
|
||||
p.poly(&[(10.0, 14.0), (17.0, 11.0), (20.5, 17.5), (13.0, 19.0)], a(STEEL));
|
||||
p.line(12.0, 18.6, 20.0, 17.6, 1.0, f(STEEL, 3));
|
||||
p.rect(9, 17, 21, 18, a(WOODM));
|
||||
p.rect(12, 17, 19, 17, f(IRON, 0));
|
||||
// chips
|
||||
p.rect(23, 9, 24, 10, f(WOODM, 2));
|
||||
p.rect(26, 13, 26, 14, f(WOODM, 2));
|
||||
}
|
||||
|
||||
fn walls(p: &mut Pad) {
|
||||
// a brick wall with battlements
|
||||
p.rect(3, 9, 28, 28, f(IRON, 1));
|
||||
for (x0, x1) in [(3, 8), (13, 18), (23, 28)] {
|
||||
p.rect(x0, 3, x1, 8, f(IRON, 1));
|
||||
}
|
||||
for (row, y) in [8, 13, 18, 23].iter().enumerate() {
|
||||
let off = if row % 2 == 0 { 0 } else { 4 };
|
||||
let mut x = 3 - off;
|
||||
while x <= 28 {
|
||||
p.rect(x.max(3), y + 1, (x + 6).min(28), y + 4, a(BRICK));
|
||||
x += 8;
|
||||
}
|
||||
}
|
||||
for (x0, x1) in [(3, 8), (13, 18), (23, 28)] {
|
||||
p.rect(x0, 4, x1, 7, a(BRICK));
|
||||
}
|
||||
p.rect(3, 9, 28, 9, f(IRON, 1));
|
||||
// a crack and a loose brick
|
||||
p.path(&[(19.0, 14.0), (17.0, 18.0), (19.0, 21.0)], 1.0, f(IRON, 0));
|
||||
}
|
||||
|
||||
fn rooms(p: &mut Pad) {
|
||||
// a stone arch with a barred wooden door
|
||||
p.disc(16.0, 15.0, 13.0, a(STONE));
|
||||
p.rect(3, 15, 28, 29, a(STONE));
|
||||
p.disc(16.0, 15.0, 8.5, f(IRON, 0));
|
||||
p.rect(8, 15, 23, 29, f(IRON, 0));
|
||||
for k in 0..7 {
|
||||
let ang = std::f32::consts::PI * (1.0 + k as f32 / 6.0);
|
||||
p.line(16.0 + 9.0 * ang.cos(), 15.0 + 9.0 * ang.sin(), 16.0 + 13.0 * ang.cos(), 15.0 + 13.0 * ang.sin(), 0.9, f(STONE, 0));
|
||||
}
|
||||
p.rect(3, 22, 7, 22, f(STONE, 0));
|
||||
p.rect(24, 22, 28, 22, f(STONE, 0));
|
||||
p.disc(16.0, 15.5, 7.5, a(WOODM));
|
||||
p.rect(9, 15, 22, 29, a(WOODM));
|
||||
for x in [12, 16, 19] {
|
||||
p.line(x as f32 + 0.5, 8.5, x as f32 + 0.5, 29.0, 0.9, f(WOODM, 0));
|
||||
}
|
||||
p.rect(9, 13, 22, 14, a(IRON));
|
||||
p.rect(9, 23, 22, 24, a(IRON));
|
||||
p.ring(20.0, 19.0, 1.2, 2.2, a(GOLDM));
|
||||
p.rect(2, 29, 29, 30, a(STONE));
|
||||
}
|
||||
|
||||
fn spells(p: &mut Pad) {
|
||||
// an open spell book with a swirl of magic
|
||||
p.poly(&[(2.0, 15.0), (16.0, 19.0), (30.0, 15.0), (30.0, 28.0), (16.0, 31.0), (2.0, 28.0)], a(PURPLE));
|
||||
p.poly(&[(3.5, 13.0), (15.5, 17.0), (15.5, 28.5), (3.5, 25.5)], a(BONE));
|
||||
p.poly(&[(16.5, 17.0), (28.5, 13.0), (28.5, 25.5), (16.5, 28.5)], a(BONE));
|
||||
for k in 0..3 {
|
||||
let y = 18.0 + k as f32 * 3.0;
|
||||
p.line(5.5, y, 13.5, y + 2.4, 0.8, f(BONE, 0));
|
||||
p.line(18.5, y + 2.4, 26.5, y, 0.8, f(BONE, 0));
|
||||
}
|
||||
p.px(5, 16, f(RED, 2));
|
||||
p.px(6, 16, f(RED, 2));
|
||||
// the swirl rising from the pages
|
||||
let mut prev = (16.0, 15.0);
|
||||
for k in 1..22 {
|
||||
let t = k as f32 / 21.0;
|
||||
let ang = t * 9.0;
|
||||
let r = 1.0 + t * 6.5;
|
||||
let pt = (16.0 + r * ang.cos(), 14.0 - t * 8.0 + r * 0.5 * ang.sin());
|
||||
p.line(prev.0, prev.1, pt.0, pt.1, 1.0 + t * 1.2, f(CYAN, if t > 0.6 { 1 } else { 2 }));
|
||||
prev = pt;
|
||||
}
|
||||
p.disc(16.0, 9.0, 1.4, f(CYAN, 3));
|
||||
p.sparkle(5, 4);
|
||||
p.sparkle(27, 5);
|
||||
p.sparkle(26, 10);
|
||||
}
|
||||
|
||||
fn slaps(p: &mut Pad) {
|
||||
// the keeper's open hand, swinging
|
||||
let tilt = 0.25f32;
|
||||
let (s, c) = tilt.sin_cos();
|
||||
let rot = |x: f32, y: f32| (19.0 + (x - 19.0) * c - (y - 19.0) * s, 19.0 + (x - 19.0) * s + (y - 19.0) * c);
|
||||
let fingers = [(13.5, 6.5), (17.0, 4.5), (20.5, 5.0), (24.0, 8.0)];
|
||||
for (x, y) in fingers {
|
||||
let (x0, y0) = rot(x, 18.0);
|
||||
let (x1, y1) = rot(x, y);
|
||||
p.line(x0, y0, x1, y1, 3.2, a(SKIN));
|
||||
p.px((x1 - 0.5) as i32, (y1 - 1.6) as i32, f(BONE, 2));
|
||||
}
|
||||
let (px0, py0) = rot(19.0, 20.0);
|
||||
p.oval(px0, py0, 6.5, 7.0, tilt, a(SKIN));
|
||||
let (tx0, ty0) = rot(13.5, 21.0);
|
||||
let (tx1, ty1) = rot(8.5, 15.0);
|
||||
p.line(tx0, ty0, tx1, ty1, 3.2, a(SKIN));
|
||||
p.px((tx1 - 1.0) as i32, (ty1 - 1.0) as i32, f(BONE, 2));
|
||||
let (wx0, wy0) = rot(17.0, 26.0);
|
||||
let (wx1, wy1) = rot(17.0, 31.0);
|
||||
p.line(wx0, wy0, wx1, wy1, 6.0, a(SKIN));
|
||||
p.line(wx0 - 3.0, wy0 + 2.5, wx0 + 3.5, wy0 + 1.0, 1.6, a(PURPLE)); // sleeve cuff
|
||||
p.line(15.5, 15.0, 19.5, 22.0, 0.8, f(SKIN, 0));
|
||||
// motion lines
|
||||
for (x0, y, x1) in [(1, 9, 6), (2, 14, 7), (1, 19, 5)] {
|
||||
p.rect(x0, y, x1, y, f(SPARK, 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn traps(p: &mut Pad) {
|
||||
// spikes shooting from a floor slab, one of them bloodied
|
||||
p.rect(2, 23, 29, 29, a(STONE));
|
||||
p.rect(2, 26, 29, 26, f(STONE, 0));
|
||||
p.rect(15, 23, 15, 26, f(STONE, 0));
|
||||
p.rect(8, 26, 8, 29, f(STONE, 0));
|
||||
p.rect(23, 26, 23, 29, f(STONE, 0));
|
||||
for (cx, tip) in [(6.0, 11.0), (11.0, 7.0), (16.0, 3.0), (21.0, 7.0), (26.0, 11.0)] {
|
||||
p.poly(&[(cx - 2.6, 23.0), (cx, tip), (cx + 2.6, 23.0)], a(STEEL));
|
||||
p.line(cx - 0.6, tip + 3.0, cx - 1.6, 22.0, 0.8, f(STEEL, 3));
|
||||
p.rect(cx as i32 - 3, 22, cx as i32 + 2, 23, f(IRON, 1));
|
||||
}
|
||||
p.poly(&[(14.6, 8.0), (16.0, 3.0), (17.4, 8.0)], a(RED));
|
||||
p.rect(16, 9, 16, 11, f(RED, 1));
|
||||
p.px(16, 13, f(RED, 2));
|
||||
}
|
||||
|
||||
fn towers(p: &mut Pad) {
|
||||
// a dark tower with a horned roof and a glowing window
|
||||
p.poly(&[(9.0, 30.0), (11.5, 12.0), (20.5, 12.0), (23.0, 30.0)], a(IRON));
|
||||
for y in [16, 21, 26] {
|
||||
p.line(11.0, y as f32 + 0.5, 21.5, y as f32 + 0.5, 0.8, f(IRON, 0));
|
||||
}
|
||||
p.rect(8, 9, 23, 12, a(PURPLE));
|
||||
for x in [8, 13, 18, 22] {
|
||||
p.rect(x, 7, x + 1, 8, a(PURPLE));
|
||||
}
|
||||
p.poly(&[(9.0, 8.0), (16.0, 0.5), (23.0, 8.0)], a(IRON));
|
||||
p.path(&[(8.5, 10.0), (5.0, 7.0), (4.0, 2.0)], 1.6, a(BONE));
|
||||
p.path(&[(23.5, 10.0), (27.0, 7.0), (28.0, 2.0)], 1.6, a(BONE));
|
||||
p.disc(16.0, 18.0, 2.2, f(FIRE, 1));
|
||||
p.rect(14, 18, 18, 22, f(FIRE, 1));
|
||||
p.rect(15, 17, 17, 21, f(FIRE, 2));
|
||||
p.px(16, 18, f(FIRE, 3));
|
||||
p.rect(14, 27, 18, 30, f(IRON, 0));
|
||||
p.disc(16.0, 27.0, 2.4, f(IRON, 0));
|
||||
p.rect(3, 30, 28, 30, a(STONE));
|
||||
}
|
||||
|
||||
fn drowned(p: &mut Pad) {
|
||||
// a helmet sinking between waves
|
||||
p.poly(&[(17.0, 2.0), (22.0, 4.0), (20.0, 6.5), (16.5, 6.0)], a(RED));
|
||||
p.disc(15.5, 14.0, 8.0, a(STEEL));
|
||||
p.rect(9, 12, 21, 13, f(IRON, 0));
|
||||
p.line(15.5, 6.0, 15.5, 20.0, 1.0, f(STEEL, 0));
|
||||
p.fill(a(WATER), |x, y| y >= 18.0 + 1.6 * (x * 0.55).sin() && y < 31.0 && (1.0..31.0).contains(&x));
|
||||
for (x0, y0) in [(4.0, 23.0), (14.0, 25.0), (22.0, 22.0), (9.0, 28.0), (20.0, 28.5)] {
|
||||
p.line(x0, y0, x0 + 3.5, y0, 1.0, f(WATER, 3));
|
||||
}
|
||||
p.ring(25.0, 12.0, 1.0, 2.0, f(GLASS, 3));
|
||||
p.ring(27.0, 6.0, 0.6, 1.5, f(GLASS, 3));
|
||||
p.px(23, 2, f(GLASS, 3));
|
||||
}
|
||||
|
||||
fn burnt(p: &mut Pad) {
|
||||
// a burning bridge plank
|
||||
p.poly(&[(2.0, 23.0), (28.0, 16.5), (30.0, 22.0), (4.0, 28.5)], a(WOODM));
|
||||
p.line(5.0, 25.5, 28.0, 19.5, 0.8, f(WOODM, 0));
|
||||
p.poly(&[(19.0, 18.8), (28.0, 16.5), (30.0, 22.0), (21.0, 24.3)], a(IRON));
|
||||
p.rect(6, 23, 7, 24, f(IRON, 2));
|
||||
p.rect(13, 21, 14, 22, f(IRON, 2));
|
||||
for (x, y) in [(20, 20), (23, 21), (25, 19), (27, 20)] {
|
||||
p.px(x, y, f(FIRE, 1));
|
||||
}
|
||||
// flames
|
||||
p.poly(&[(14.0, 21.0), (13.0, 14.0), (16.0, 9.0), (17.0, 13.0), (20.0, 4.0), (22.0, 11.0), (24.5, 8.0), (25.5, 14.0), (28.0, 17.0)], f(FIRE, 0));
|
||||
p.poly(&[(16.0, 20.0), (16.0, 15.0), (18.0, 12.5), (19.0, 15.0), (20.5, 9.0), (22.0, 14.0), (24.0, 12.0), (24.5, 16.0), (26.0, 18.0)], f(FIRE, 1));
|
||||
p.poly(&[(18.5, 19.0), (19.0, 16.0), (20.5, 13.5), (21.5, 16.0), (23.0, 15.5), (23.5, 18.0)], f(FIRE, 2));
|
||||
p.px(21, 17, f(FIRE, 3));
|
||||
for (x, y) in [(11, 6), (14, 3), (26, 4), (29, 10), (8, 12)] {
|
||||
p.px(x, y, f(EMBER, 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn siege(p: &mut Pad) {
|
||||
// a catapult with a boulder
|
||||
p.line(13.0, 24.0, 17.0, 13.0, 2.0, a(DKWOOD));
|
||||
p.line(21.0, 24.0, 17.0, 13.0, 2.0, a(DKWOOD));
|
||||
p.line(6.0, 22.0, 24.0, 7.0, 2.2, a(WOODM));
|
||||
p.oval(24.5, 6.5, 3.2, 2.0, -0.7, a(DKWOOD));
|
||||
p.disc(25.0, 3.5, 2.8, a(STONE));
|
||||
p.rect(3, 22, 28, 24, a(WOODM));
|
||||
p.rect(3, 23, 28, 23, f(WOODM, 0));
|
||||
p.disc(17.0, 13.0, 1.4, a(IRON));
|
||||
p.line(6.0, 22.0, 10.0, 22.0, 1.0, f(IRON, 1)); // the rope
|
||||
for cx in [8.0, 23.0] {
|
||||
p.disc(cx, 25.5, 4.0, a(DKWOOD));
|
||||
p.ring(cx, 25.5, 2.6, 3.3, f(DKWOOD, 0));
|
||||
p.disc(cx, 25.5, 1.2, a(STEEL));
|
||||
}
|
||||
p.rect(1, 30, 30, 30, a(STONE));
|
||||
}
|
||||
|
||||
fn captured(p: &mut Pad) {
|
||||
// two iron shackles joined by a chain
|
||||
p.ring(9.0, 20.0, 3.5, 6.5, a(STEEL));
|
||||
p.ring(23.0, 20.0, 3.5, 6.5, a(STEEL));
|
||||
p.rect(7, 13, 11, 15, a(IRON));
|
||||
p.rect(21, 13, 25, 15, a(IRON));
|
||||
p.disc(9.0, 24.5, 1.2, f(IRON, 0));
|
||||
p.disc(23.0, 24.5, 1.2, f(IRON, 0));
|
||||
// the chain, links turned alternately
|
||||
let links = [(9.0, 11.0, true), (12.0, 7.5, false), (16.0, 6.0, true), (20.0, 7.5, false), (23.0, 11.0, true)];
|
||||
for (x, y, upright) in links {
|
||||
if upright {
|
||||
p.oval(x, y, 1.6, 2.6, 0.0, a(STEEL));
|
||||
p.rect(x as i32, y as i32 - 1, x as i32, y as i32, f(IRON, 0));
|
||||
} else {
|
||||
p.oval(x, y, 2.6, 1.6, 0.0, a(IRON));
|
||||
p.rect(x as i32 - 1, y as i32, x as i32, y as i32, f(IRON, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drawbridge(p: &mut Pad) {
|
||||
// a gatehouse with its bridge hanging on chains above the moat
|
||||
p.rect(14, 4, 29, 29, a(STONE));
|
||||
for x in [14, 19, 24] {
|
||||
p.rect(x, 1, x + 2, 3, a(STONE));
|
||||
}
|
||||
for y in [9, 15, 21] {
|
||||
p.rect(14, y, 29, y, f(STONE, 0));
|
||||
}
|
||||
p.disc(22.0, 16.0, 4.5, f(IRON, 0));
|
||||
p.rect(18, 16, 26, 28, f(IRON, 0));
|
||||
for x in [19, 21, 23, 25] {
|
||||
p.rect(x, 13, x, 22, f(IRON, 2));
|
||||
}
|
||||
p.fill(a(WATER), |x, y| y >= 26.5 + 0.8 * (x * 1.1).sin() && y < 31.0 && (1.0..18.0).contains(&x));
|
||||
// the bridge, half raised
|
||||
p.line(18.0, 27.0, 4.0, 17.0, 4.0, a(WOODM));
|
||||
for k in 1..4 {
|
||||
let t = k as f32 / 4.0;
|
||||
let (x, y) = (18.0 - 14.0 * t, 27.0 - 10.0 * t);
|
||||
p.line(x - 1.2, y - 1.7, x + 1.2, y + 1.7, 0.8, f(WOODM, 0));
|
||||
}
|
||||
p.disc(18.0, 27.0, 1.3, a(IRON));
|
||||
for k in 0..6 {
|
||||
let t = k as f32 / 5.0;
|
||||
let (x, y) = (4.5 + 9.5 * t, 15.0 - 7.0 * t);
|
||||
p.disc(x, y, 1.1, if k % 2 == 0 { a(STEEL) } else { a(IRON) });
|
||||
}
|
||||
}
|
||||
|
||||
fn wins(p: &mut Pad) {
|
||||
// a banner planted in the dungeon heart
|
||||
p.disc(10.5, 20.0, 6.0, a(RED));
|
||||
p.disc(21.5, 20.0, 6.0, a(RED));
|
||||
p.poly(&[(4.7, 21.5), (27.3, 21.5), (16.0, 31.0)], a(RED));
|
||||
p.oval(9.0, 18.5, 2.0, 1.4, -0.6, f(RED, 3));
|
||||
p.line(12.0, 2.0, 12.0, 21.0, 1.8, a(DKWOOD));
|
||||
p.disc(12.0, 2.0, 1.3, a(GOLDM));
|
||||
p.poly(&[(13.0, 3.5), (27.0, 4.5), (23.5, 8.0), (27.0, 11.5), (13.0, 12.5)], a(GOLDM));
|
||||
p.line(14.5, 6.0, 21.5, 6.5, 0.8, f(GOLDM, 0));
|
||||
p.line(14.5, 9.5, 21.5, 9.5, 0.8, f(GOLDM, 0));
|
||||
}
|
||||
|
||||
fn missions(p: &mut Pad) {
|
||||
// a crystal shard on a rock
|
||||
p.poly(&[(5.0, 30.0), (8.0, 25.0), (14.0, 24.0), (22.0, 25.0), (27.0, 30.0)], a(STONE));
|
||||
p.poly(&[(16.0, 1.5), (22.0, 9.0), (19.5, 27.0), (12.5, 27.0), (10.0, 9.0)], f(CRYSTAL, 1));
|
||||
p.poly(&[(16.0, 1.5), (10.0, 9.0), (12.5, 27.0), (14.5, 27.0), (14.0, 9.0)], f(CRYSTAL, 2));
|
||||
p.poly(&[(16.0, 1.5), (22.0, 9.0), (19.5, 27.0), (18.5, 27.0), (18.5, 9.0)], f(CRYSTAL, 0));
|
||||
p.line(12.0, 6.5, 13.0, 18.0, 0.9, f(CRYSTAL, 3));
|
||||
p.poly(&[(6.0, 14.0), (9.5, 18.0), (10.5, 27.0), (6.5, 27.0), (4.5, 19.0)], f(CRYSTAL, 1));
|
||||
p.poly(&[(6.0, 14.0), (4.5, 19.0), (6.5, 27.0), (7.5, 27.0), (6.5, 18.0)], f(CRYSTAL, 2));
|
||||
p.poly(&[(26.0, 16.0), (28.0, 21.0), (26.5, 27.0), (22.0, 27.0), (23.0, 20.0)], f(CRYSTAL, 0));
|
||||
p.sparkle(24, 5);
|
||||
p.sparkle(4, 8);
|
||||
}
|
||||
|
||||
fn coop(p: &mut Pad) {
|
||||
// two claws clasped: an imp's and a demon's
|
||||
p.line(0.0, 25.0, 11.0, 19.5, 6.0, a(SKIN));
|
||||
p.line(32.0, 25.0, 21.0, 19.5, 6.0, a(RED));
|
||||
p.line(3.0, 21.0, 6.0, 27.5, 2.0, a(PURPLE)); // cuffs
|
||||
p.line(29.0, 21.0, 26.0, 27.5, 2.0, a(IRON));
|
||||
p.oval(19.5, 18.5, 5.5, 4.5, 0.0, a(RED));
|
||||
p.oval(12.5, 18.5, 5.0, 4.8, 0.0, a(SKIN));
|
||||
// the imp's fingers curl over the back of the demon's hand
|
||||
for k in 0..4 {
|
||||
let x = 15.0 + k as f32 * 2.4;
|
||||
p.path(&[(x - 0.5, 15.0), (x + 1.0, 17.5), (x + 0.6, 21.0)], 2.0, a(SKIN));
|
||||
p.px((x + 0.6) as i32, 22, f(BONE, 3));
|
||||
}
|
||||
// the demon's thumb over the imp's hand
|
||||
p.line(19.0, 14.5, 12.0, 14.0, 2.2, a(RED));
|
||||
p.px(10, 13, f(BONE, 3));
|
||||
p.px(11, 13, f(BONE, 2));
|
||||
p.sparkle(16, 6);
|
||||
p.sparkle(8, 9);
|
||||
p.sparkle(25, 10);
|
||||
}
|
||||
|
||||
fn hourglass(p: &mut Pad, cx: f32, top: i32, bottom: i32, half: f32, frame: u8) {
|
||||
let (t, b) = (top as f32, bottom as f32);
|
||||
let mid = (t + b) * 0.5 + 0.5;
|
||||
p.poly(&[(cx - half + 1.0, t + 2.0), (cx + half - 1.0, t + 2.0), (cx + 1.0, mid), (cx + half - 1.0, b - 1.0), (cx - half + 1.0, b - 1.0), (cx - 1.0, mid)], f(GLASS, 1));
|
||||
p.poly(&[(cx - half + 2.0, t + 2.5), (cx - half + 3.0, t + 2.5), (cx - 0.5, mid - 1.0)], f(GLASS, 3));
|
||||
// sand: a little left above, a heap below, the stream
|
||||
p.poly(&[(cx - half * 0.45, mid - 4.0), (cx + half * 0.45, mid - 4.0), (cx, mid - 0.5)], f(SUN, 1));
|
||||
p.poly(&[(cx - half + 1.5, b - 1.0), (cx, b - 6.0), (cx + half - 1.5, b - 1.0)], f(SUN, 1));
|
||||
p.poly(&[(cx - 2.0, b - 3.5), (cx, b - 6.0), (cx + 0.5, b - 3.5)], f(SUN, 2));
|
||||
p.line(cx, mid, cx, b - 5.0, 0.9, f(SUN, 2));
|
||||
p.rect((cx - half) as i32, top, (cx + half) as i32 - 1, top + 1, a(frame));
|
||||
p.rect((cx - half) as i32, bottom, (cx + half) as i32 - 1, bottom + 1, a(frame));
|
||||
p.line(cx - half + 0.5, t + 1.0, cx - half + 0.5, b, 1.3, a(frame));
|
||||
p.line(cx + half - 0.5, t + 1.0, cx + half - 0.5, b, 1.3, a(frame));
|
||||
}
|
||||
|
||||
fn minutes(p: &mut Pad) {
|
||||
// an hourglass with a candle beside it
|
||||
hourglass(p, 13.0, 3, 27, 8.0, WOODM);
|
||||
p.rect(23, 15, 27, 28, a(BONE));
|
||||
p.rect(22, 28, 28, 29, a(IRON));
|
||||
p.line(24.0, 17.0, 24.0, 21.0, 0.9, f(BONE, 3));
|
||||
p.px(25, 14, f(IRON, 0));
|
||||
p.poly(&[(23.5, 13.0), (25.0, 7.0), (26.6, 13.0)], f(FIRE, 1));
|
||||
p.poly(&[(24.4, 13.0), (25.0, 9.5), (25.8, 13.0)], f(FIRE, 3));
|
||||
}
|
||||
|
||||
fn flawless(p: &mut Pad) {
|
||||
// a gleaming, untouched shield
|
||||
let rim = [(5.0, 3.0), (27.0, 3.0), (27.0, 15.0), (24.0, 22.0), (16.0, 30.0), (8.0, 22.0), (5.0, 15.0)];
|
||||
p.poly(&rim, a(GOLDM));
|
||||
let field = [(7.5, 5.5), (24.5, 5.5), (24.5, 15.0), (22.0, 21.0), (16.0, 27.0), (10.0, 21.0), (7.5, 15.0)];
|
||||
p.poly(&field, a(BLUE));
|
||||
p.poly(&[(7.5, 5.5), (16.0, 5.5), (16.0, 27.0), (10.0, 21.0), (7.5, 15.0)], f(BLUE, 2));
|
||||
p.rect(15, 7, 16, 23, a(GOLDM));
|
||||
p.rect(10, 12, 21, 13, a(GOLDM));
|
||||
p.disc(16.0, 12.5, 2.0, a(CRYSTAL));
|
||||
p.line(9.0, 7.0, 12.0, 7.0, 0.9, f(BLUE, 3));
|
||||
p.line(9.0, 7.0, 9.0, 10.0, 0.9, f(BLUE, 3));
|
||||
p.sparkle(26, 4);
|
||||
p.sparkle(4, 20);
|
||||
p.px(28, 9, f(SPARK, 2));
|
||||
}
|
||||
|
||||
fn speedrun(p: &mut Pad) {
|
||||
// a winged hourglass
|
||||
for side in [-1.0f32, 1.0] {
|
||||
let x = |dx: f32| 16.0 + side * dx;
|
||||
p.poly(&[(x(6.0), 10.0), (x(13.0), 4.0), (x(15.5), 6.0), (x(14.5), 10.0), (x(15.0), 13.0), (x(13.0), 17.0), (x(6.0), 19.0)], a(BONE));
|
||||
for (dx, y) in [(10.0, 9.0), (11.0, 12.0), (10.0, 15.0)] {
|
||||
p.line(x(6.5), y + 1.5, x(dx + 4.0), y, 0.8, f(BONE, 0));
|
||||
}
|
||||
}
|
||||
hourglass(p, 16.0, 4, 27, 6.0, GOLDM);
|
||||
p.path(&[(27.0, 20.0), (25.0, 24.0), (28.0, 24.0), (25.5, 29.5)], 1.2, f(SUN, 2));
|
||||
p.path(&[(6.0, 20.0), (4.0, 24.0), (7.0, 24.0), (4.5, 29.5)], 1.2, f(SUN, 2));
|
||||
}
|
||||
|
||||
fn campaign(p: &mut Pad) {
|
||||
// the crown with the core made whole at its heart
|
||||
p.poly(&[(4.0, 22.0), (3.0, 8.0), (10.0, 15.0), (16.0, 5.0), (22.0, 15.0), (29.0, 8.0), (28.0, 22.0)], a(GOLDM));
|
||||
p.rect(4, 21, 27, 27, a(GOLDM));
|
||||
p.rect(4, 22, 27, 22, f(GOLDM, 0));
|
||||
p.rect(4, 26, 27, 26, f(GOLDM, 0));
|
||||
for (x, y) in [(3.0, 7.0), (16.0, 3.5), (29.0, 7.0)] {
|
||||
p.disc(x, y, 1.8, a(GOLDM));
|
||||
}
|
||||
p.disc(8.5, 24.0, 1.3, a(RED));
|
||||
p.disc(23.5, 24.0, 1.3, a(RED));
|
||||
// the core
|
||||
p.poly(&[(16.0, 11.0), (20.5, 17.5), (16.0, 26.0), (11.5, 17.5)], f(CRYSTAL, 1));
|
||||
p.poly(&[(16.0, 11.0), (11.5, 17.5), (16.0, 26.0), (15.0, 17.5)], f(CRYSTAL, 2));
|
||||
p.px(14, 15, f(CRYSTAL, 3));
|
||||
p.px(14, 16, f(CRYSTAL, 3));
|
||||
p.sparkle(26, 14);
|
||||
p.sparkle(7, 13);
|
||||
p.sparkle(16, 0);
|
||||
}
|
||||
|
||||
fn surface(p: &mut Pad) {
|
||||
// the sun rising over grassy hills
|
||||
for k in 0..8 {
|
||||
let ang = k as f32 * std::f32::consts::PI / 4.0;
|
||||
p.line(16.0 + 9.0 * ang.cos(), 14.0 + 9.0 * ang.sin(), 16.0 + 13.0 * ang.cos(), 14.0 + 13.0 * ang.sin(), 2.0, f(SUN, 1));
|
||||
}
|
||||
p.disc(16.0, 14.0, 7.0, f(SUN, 2));
|
||||
p.disc(14.5, 12.5, 4.0, f(SUN, 3));
|
||||
p.fill(a(GREEN), |x, y| y >= 21.0 + 2.5 * (x * 0.22 + 0.4).sin() && y < 31.0 && (1.0..31.0).contains(&x));
|
||||
for x in [3, 7, 12, 19, 24, 28] {
|
||||
let base = 21.0 + 2.5 * ((x as f32 + 0.5) * 0.22 + 0.4).sin();
|
||||
p.line(x as f32 + 0.5, base, x as f32 - 0.5, base - 3.0, 1.0, f(GREEN, 2));
|
||||
}
|
||||
p.px(10, 26, f(RED, 2));
|
||||
p.px(22, 27, f(SUN, 2));
|
||||
p.px(16, 28, f(SPARK, 1));
|
||||
}
|
||||
|
||||
fn defeat(p: &mut Pad) {
|
||||
// a cup of tea, to soothe the defeat
|
||||
p.oval(16.0, 27.0, 14.0, 2.8, 0.0, a(BONE));
|
||||
p.oval(16.0, 26.5, 9.0, 1.5, 0.0, f(BONE, 0));
|
||||
p.ring(24.5, 18.5, 2.0, 4.0, a(BONE));
|
||||
p.poly(&[(6.0, 13.0), (26.0, 13.0), (25.0, 20.0), (21.0, 26.0), (11.0, 26.0), (7.0, 20.0)], a(BONE));
|
||||
p.rect(7, 16, 25, 17, f(BLUE, 2));
|
||||
p.px(12, 16, f(BLUE, 0));
|
||||
p.px(16, 17, f(BLUE, 0));
|
||||
p.px(20, 16, f(BLUE, 0));
|
||||
p.oval(16.0, 13.5, 9.5, 1.8, 0.0, f(TEA, 1));
|
||||
p.oval(14.0, 13.0, 3.0, 0.6, 0.0, f(TEA, 3));
|
||||
// steam
|
||||
for x0 in [11.0f32, 16.0, 21.0] {
|
||||
let mut prev = (x0, 10.0);
|
||||
for k in 1..6 {
|
||||
let y = 10.0 - k as f32 * 1.5;
|
||||
let pt = (x0 + (k as f32 * 1.4).sin() * 1.3, y);
|
||||
p.line(prev.0, prev.1, pt.0, pt.1, 0.9, f(EMBER, 3));
|
||||
prev = pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn legend(p: &mut Pad) {
|
||||
// a star crowned with laurels
|
||||
for side in [-1.0f32, 1.0] {
|
||||
let mut prev = (16.0 + side * 3.0, 29.0);
|
||||
for k in 0..7 {
|
||||
let ang = std::f32::consts::FRAC_PI_2 + side * (0.35 + k as f32 * 0.33);
|
||||
let pt = (16.0 + 12.0 * ang.cos(), 16.0 + 12.0 * ang.sin());
|
||||
p.line(prev.0, prev.1, pt.0, pt.1, 1.0, f(GREEN, 0));
|
||||
let out = (16.0 + 14.2 * ang.cos(), 16.0 + 14.2 * ang.sin());
|
||||
let inn = (16.0 + 10.0 * ang.cos(), 16.0 + 10.0 * ang.sin());
|
||||
p.oval(out.0, out.1, 2.2, 1.1, ang + side * 0.6, a(GREEN));
|
||||
if k < 6 {
|
||||
p.oval(inn.0, inn.1, 2.0, 1.0, ang - side * 0.6, a(GREEN));
|
||||
}
|
||||
prev = pt;
|
||||
}
|
||||
}
|
||||
p.line(13.0, 29.0, 19.0, 29.0, 1.4, a(RED));
|
||||
p.star(16.0, 15.5, 4.2, 10.0, a(GOLDM));
|
||||
p.star(16.0, 15.5, 1.8, 4.0, f(GOLDM, 3));
|
||||
}
|
||||
|
||||
fn wyrm(p: &mut Pad) {
|
||||
// the ember wyrm's head
|
||||
p.path(&[(10.0, 9.0), (6.0, 5.0), (2.0, 4.0)], 2.2, a(BONE));
|
||||
p.path(&[(13.0, 8.0), (11.0, 3.0), (7.0, 0.5)], 2.0, a(BONE));
|
||||
// neck and spines
|
||||
p.poly(&[(2.0, 31.0), (4.0, 18.0), (9.0, 10.0), (14.0, 22.0), (12.0, 31.0)], a(RED));
|
||||
for (x, y) in [(3.0, 22.0), (3.5, 27.0), (5.5, 16.0)] {
|
||||
p.poly(&[(x, y), (x - 3.0, y + 0.5), (x + 0.5, y + 3.0)], a(FIRE));
|
||||
}
|
||||
// the head, snout to the right, jaw open
|
||||
p.poly(&[(6.0, 12.0), (12.0, 7.0), (21.0, 9.0), (29.0, 12.0), (29.5, 15.0), (19.0, 16.5), (12.0, 20.0)], a(RED));
|
||||
p.poly(&[(12.0, 19.0), (19.0, 18.0), (27.0, 21.0), (26.0, 23.5), (16.0, 24.0), (10.0, 22.0)], a(RED));
|
||||
p.poly(&[(19.0, 16.4), (29.0, 15.0), (27.0, 20.5), (19.0, 18.0)], f(IRON, 0));
|
||||
for x in [21, 24, 27] {
|
||||
p.px(x, 16, f(BONE, 3));
|
||||
p.px(x - 1, 19, f(BONE, 3));
|
||||
}
|
||||
p.oval(16.0, 11.5, 2.4, 1.4, 0.2, f(SUN, 2));
|
||||
p.rect(16, 11, 16, 12, f(IRON, 0));
|
||||
p.line(13.0, 9.6, 19.0, 10.0, 0.9, f(RED, 0));
|
||||
p.px(27, 11, f(IRON, 0));
|
||||
p.line(16.0, 22.0, 25.0, 21.5, 0.8, f(RED, 3));
|
||||
// embers from the jaws
|
||||
for (x, y) in [(30, 18), (28, 25), (30, 27), (24, 27), (29, 22)] {
|
||||
p.px(x, y, f(EMBER, 1));
|
||||
}
|
||||
}
|
||||
|
||||
fn fullhand(p: &mut Pad) {
|
||||
// the keeper's hand, full of creatures
|
||||
// horns and ears behind the heads
|
||||
p.poly(&[(12.5, 7.5), (11.0, 2.5), (14.5, 6.0)], a(BONE));
|
||||
p.poly(&[(19.5, 7.5), (21.0, 2.5), (17.5, 6.0)], a(BONE));
|
||||
p.poly(&[(6.0, 14.0), (3.5, 10.0), (8.0, 12.5)], a(GREEN));
|
||||
p.poly(&[(26.0, 14.0), (28.5, 10.0), (24.0, 12.5)], a(PURPLE));
|
||||
let heads: [(f32, f32, u8); 6] = [(16.0, 10.0, RED), (9.0, 16.5, GREEN), (23.0, 16.5, PURPLE), (16.0, 16.0, BONE), (10.5, 22.0, STEEL), (21.5, 22.0, BRICK)];
|
||||
for (x, y, m) in heads {
|
||||
p.disc(x, y, 4.3, f(IRON, 0));
|
||||
p.disc(x, y, 3.6, a(m));
|
||||
let (ex, ey) = (x as i32, y as i32);
|
||||
p.px(ex - 2, ey, f(SUN, 2));
|
||||
p.px(ex + 1, ey, f(SUN, 2));
|
||||
p.px(ex - 1, ey + 2, f(IRON, 0));
|
||||
p.px(ex, ey + 2, f(IRON, 0));
|
||||
}
|
||||
// the cupped hand below
|
||||
p.poly(&[(2.0, 22.0), (6.0, 25.0), (26.0, 25.0), (30.0, 21.0), (29.0, 27.0), (22.0, 30.5), (9.0, 30.5), (3.0, 27.0)], a(SKIN));
|
||||
p.line(30.0, 21.0, 29.0, 15.0, 3.0, a(SKIN));
|
||||
p.px(28, 13, f(BONE, 3));
|
||||
p.line(2.0, 22.0, 3.0, 17.0, 3.0, a(SKIN));
|
||||
p.px(3, 15, f(BONE, 3));
|
||||
p.line(8.0, 27.5, 24.0, 27.5, 0.8, f(SKIN, 0));
|
||||
}
|
||||
|
||||
// every badge in order of its index
|
||||
pub fn gen_badges() -> Image {
|
||||
let rows = (COUNT + COLS - 1) / COLS;
|
||||
let mut img = Image::gen_image_color((COLS * SIZE) as u16, (rows * SIZE) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||
let painters: [fn(&mut Pad); COUNT as usize] = [
|
||||
heroes, bosses, dug, gold, wood, walls, rooms, spells, slaps, traps, towers, drowned, burnt, siege, captured, drawbridge, wins, missions, coop, minutes, flawless, speedrun, campaign, surface, defeat, legend, wyrm, fullhand,
|
||||
];
|
||||
for (i, paint) in painters.iter().enumerate() {
|
||||
let mut p = Pad::new();
|
||||
paint(&mut p);
|
||||
p.blit(&mut img, i as u32);
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_badge_is_drawn() {
|
||||
let img = gen_badges();
|
||||
for i in 0..COUNT {
|
||||
let r = badge_rect(i);
|
||||
let mut opaque = 0;
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
if img.get_pixel(r.x as u32 + x, r.y as u32 + y).a > 0.5 {
|
||||
opaque += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(opaque > 150, "badge {} has only {} pixels", i, opaque);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn export_badges_png() {
|
||||
// look at the art: cargo test export_badges_png -- --ignored
|
||||
let img = gen_badges();
|
||||
let mut big = Image::gen_image_color(img.width * 4, img.height * 4, Color::new(0.1, 0.07, 0.07, 1.0));
|
||||
for y in 0..img.height as u32 * 4 {
|
||||
for x in 0..img.width as u32 * 4 {
|
||||
let c = img.get_pixel(x / 4, img.height as u32 - 1 - y / 4); // export_png flips
|
||||
if c.a > 0.0 {
|
||||
big.set_pixel(x, y, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
big.export_png("/tmp/badges.png");
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-7
@@ -10,13 +10,13 @@ use serde::{Deserialize, Serialize};
|
||||
pub const SIDES: [i32; 6] = [-1, 0, 1, 2, 3, 4];
|
||||
|
||||
// creature and hero kinds a block may name ("" = any)
|
||||
pub const KINDS: [&str; 26] = [
|
||||
pub const KINDS: [&str; 30] = [
|
||||
"Worker", "Kobold", "Spider", "Mage", "Ogre", "Jailer", "Priestess", "Chainbreaker", "Bat", "Gargoyle", "Owl", "Wyrm", "Cogling", "Revenant", "Knight", "Archer", "Priest", "Dwarf", "Paladin", "Giant", "Fairy",
|
||||
"BattleMage", "Ram", "Catapult", "CaveBear", "Beetle",
|
||||
"BattleMage", "Ram", "Catapult", "CaveBear", "Beetle", "Thief", "Monk", "Ranger", "Inquisitor",
|
||||
];
|
||||
|
||||
// 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));
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
let i = idx(tx, ty);
|
||||
if !self.wooden_bridge(i) || self.burning(i) {
|
||||
// nothing catches fire in the rain
|
||||
if !self.wooden_bridge(i) || self.burning(i) || self.rain_at(i) {
|
||||
return;
|
||||
}
|
||||
self.bridgework.burning.push((i, 0.0));
|
||||
|
||||
+6
-2
@@ -84,6 +84,8 @@ pub struct Carry {
|
||||
pub rune_free: u32, // unbuilt rune forge towers from chests
|
||||
#[serde(default)]
|
||||
pub rune_final: u32, // unbuilt or sold rune forge towers that were already given back once
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents, // the master's talents go along from mission to mission
|
||||
}
|
||||
|
||||
pub const CARRY_CREATURES: usize = 5;
|
||||
@@ -580,6 +582,7 @@ fn start_mission_def(d: &MissionDef, i: usize, carry: Option<&Carry>, coop: bool
|
||||
g.gold += c.gold;
|
||||
g.rune_free += c.rune_free;
|
||||
g.rune_final += c.rune_final;
|
||||
g.talents = c.talents.clone();
|
||||
}
|
||||
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
|
||||
let a = k as f32 * 0.9;
|
||||
@@ -796,7 +799,7 @@ pub fn carry_from(g: &Game) -> Carry {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
|
||||
let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval;
|
||||
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal));
|
||||
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next }
|
||||
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next, talents: g.talents.clone() }
|
||||
}
|
||||
|
||||
// a co-op match of an editor campaign: what every master takes into the next mission - his
|
||||
@@ -810,7 +813,7 @@ pub fn coop_carry_from(g: &Game) -> Vec<(u8, Carry)> {
|
||||
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && !e.hero && e.side == side && e.kind != EK::Worker && !e.chained && e.captive == 0).collect();
|
||||
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal).then(a.id.cmp(&b.id)));
|
||||
let creatures = c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect();
|
||||
(side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0 })
|
||||
(side, Carry { creatures, gold: (g.gold_of(side).max(0.0) * CARRY_GOLD).floor(), rune_free: 0, rune_final: 0, talents: g.talents_of(side) })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -824,6 +827,7 @@ pub fn apply_coop_carry(g: &mut Game, carry: &[(u8, Carry)]) {
|
||||
}
|
||||
g.switch_to(*side);
|
||||
g.gold += c.gold.max(0.0);
|
||||
g.talents = c.talents.clone();
|
||||
let (cx, cy) = g.map.core;
|
||||
for (k, &(kind, level, xp)) in c.creatures.iter().take(CARRY_CREATURES).enumerate() {
|
||||
let a = k as f32 * 1.1;
|
||||
|
||||
+14
-6
@@ -101,6 +101,10 @@ pub enum Act {
|
||||
to: u8,
|
||||
amount: u32,
|
||||
},
|
||||
// spend a talent point on a branch (talents.rs)
|
||||
Talent {
|
||||
branch: u8,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -148,7 +152,7 @@ impl Game {
|
||||
|
||||
fn mark_command(&mut self, act: Act, side: u8) {
|
||||
let ((x, y), kind, dur) = match act {
|
||||
Act::SendGold { .. } => return, // nothing to point at on the map
|
||||
Act::SendGold { .. } | Act::Talent { .. } => return, // nothing to point at on the map
|
||||
Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0),
|
||||
Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0),
|
||||
Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0),
|
||||
@@ -227,6 +231,7 @@ impl Game {
|
||||
Act::CancelOrders => self.cancel_orders(),
|
||||
Act::Unmark { x, y } => self.unmark(x, y),
|
||||
Act::SendGold { to, amount } => self.send_gold(to, amount),
|
||||
Act::Talent { branch } => self.learn_talent(branch as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +288,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_order_flags_do_not_need_the_debug_marker_setting() {
|
||||
fn command_markers_only_show_with_the_debug_setting() {
|
||||
let mut g = game(13);
|
||||
g.debug_markers = false;
|
||||
let (cx, cy) = g.map.core;
|
||||
@@ -291,10 +296,13 @@ mod tests {
|
||||
g.queue(Act::Guard { x: cx, y: cy });
|
||||
g.queue(Act::Attack { ent: None, x: (cx + 20) as f32, y: (cy + 20) as f32 });
|
||||
g.queue(Act::Flag { x: cx, y: cy });
|
||||
assert!(!g.marks.iter().any(|m| m.kind == MARK_DIG), "dig stays hidden without debug markers");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.fixed), "guard is fixed");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.fixed), "attack is fixed");
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_FLAG && m.fixed), "gather flag is fixed");
|
||||
assert!(g.marks.is_empty(), "the real shields, swords and flags show the orders");
|
||||
// with the setting on they show, and removing a marker takes them away
|
||||
g.debug_markers = true;
|
||||
g.queue(Act::Guard { x: cx + 2, y: cy + 2 });
|
||||
assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD));
|
||||
g.unmark(cx as f32 + 2.5, cy as f32 + 2.5);
|
||||
assert!(!g.marks.iter().any(|m| m.kind == MARK_GUARD), "gone with the marker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+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,9 +719,23 @@ 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
|
||||
struct Lay {
|
||||
|
||||
@@ -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
|
||||
|
||||
+9
-1
@@ -35,7 +35,7 @@ enum Cat {
|
||||
|
||||
const CATS: [Cat; 9] = [Cat::Terrain, Cat::Rooms, Cat::Places, Cat::Units, Cat::Towers, Cat::Traps, Cat::Forts, Cat::Zones, Cat::Erase];
|
||||
|
||||
const TERRAIN: [&str; 16] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel", "mushroom", "grass", "tree"];
|
||||
const TERRAIN: [&str; 17] = ["floor", "earth", "reinforced", "rock", "water", "lava", "bridge", "lavabridge", "gold", "silver", "ruby", "diamond", "steel", "mushroom", "grove", "grass", "tree"];
|
||||
const PLACES: [&str; 7] = ["core", "portal", "gate", "spring", "shard", "chest", "chained"];
|
||||
const OWNERS: [Owner; 6] = [Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3, Owner::None];
|
||||
const ZONE_SHAPES: [&str; 4] = ["rect", "circle", "tiles", "line"];
|
||||
@@ -76,6 +76,7 @@ fn kind_of_terrain(t: &str) -> (Kind, bool) {
|
||||
"bridge" => (Kind::Bridge, false),
|
||||
"lavabridge" => (Kind::LavaBridge, false),
|
||||
"mushroom" => (Kind::Mushroom, false),
|
||||
"grove" => (Kind::Grove, false),
|
||||
"grass" => (Kind::Floor, false),
|
||||
"tree" => (Kind::Tree, false),
|
||||
"gold" => (Kind::Gold, false),
|
||||
@@ -218,6 +219,13 @@ impl MapEdit {
|
||||
self.game.torches.retain(|tr| tr.wall != i);
|
||||
let owner = if k == Kind::Floor || reinforced || matches!(k, Kind::Bridge | Kind::LavaBridge) { OWNERS[self.owner] } else { Owner::None };
|
||||
let tl = &mut self.game.map.t[i];
|
||||
// a mushroom painted into a grove belongs to it (it leaves moss when felled); fully grown
|
||||
if k == Kind::Mushroom {
|
||||
tl.res = if tl.kind == Kind::Grove || tl.res & crate::map::MUSH_GROVE != 0 { crate::map::MUSH_GROVE } else { 0 };
|
||||
tl.food = 0.0;
|
||||
} else if tl.kind == Kind::Mushroom {
|
||||
tl.res = 0;
|
||||
}
|
||||
tl.kind = k;
|
||||
tl.reinforced = reinforced;
|
||||
tl.owner = if reinforced && owner == Owner::None { Owner::Player } else { owner };
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ impl Game {
|
||||
}
|
||||
}
|
||||
// what the creatures and towers see along open ground (walls stop the view)
|
||||
let mut eyes: Vec<(usize, i32)> = self.ents.iter().filter(|e| e.alive && !e.held && !e.hero && (e.side == self.me_side || self.allied(self.me_side, e.side)) && !e.chained).map(|e| (e.tile(), SIGHT)).collect();
|
||||
let mut eyes: Vec<(usize, i32)> = self.ents.iter().filter(|e| e.alive && !e.held && !e.hero && (e.side == self.me_side || self.allied(self.me_side, e.side)) && !e.chained).map(|e| (e.tile(), self.sight_at(e.tile(), SIGHT))).collect();
|
||||
let solo = self.masters.is_empty();
|
||||
eyes.extend(self.towers.iter().filter(|t| !t.hero && (solo || allies.contains(&self.map.t[t.tile].owner))).map(|t| (t.tile, t.range() as i32 + 1)));
|
||||
let mut stamp = vec![0u32; n];
|
||||
|
||||
+297
-40
@@ -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 {
|
||||
@@ -94,6 +103,7 @@ pub struct SfxEvent {
|
||||
pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI
|
||||
}
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const MAX_CREATURES: usize = 100; // creatures of one master (the beds decide below that)
|
||||
pub const SPRING_MANA: f32 = 0.6; // mana per second of each claimed mana spring
|
||||
pub const SPRING_MAX: f32 = 150.0; // more mana maximum per claimed spring
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
@@ -131,6 +141,11 @@ pub enum EK {
|
||||
CaveBear, // strong and shaggy, likes the mushroom garden
|
||||
Beetle, // a giant armoured beetle, likes the lair
|
||||
Trader, // a merchant visiting through the portal (traders.rs)
|
||||
// heroes of the later waves (rogues.rs)
|
||||
Thief, // fast, sneaks to a treasury, steals gold and runs off with it
|
||||
Monk, // fists and faith: spells do not touch him
|
||||
Ranger, // a bow from afar, lays snares in the dungeon's corridors
|
||||
Inquisitor, // fragile; now and then turns one of the weakest creatures to the heroes' side
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -146,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 };
|
||||
@@ -178,6 +195,10 @@ pub fn st(k: EK) -> St {
|
||||
EK::CaveBear => s("unit.cavebear", 420.0, 24.0, 1.0, 1.9, 1.4, 110, 0),
|
||||
EK::Beetle => s("unit.beetle", 300.0, 13.0, 1.0, 1.7, 1.1, 70, 0),
|
||||
EK::Trader => s("unit.trader", 200.0, 1.0, 1.0, 1.5, 2.0, 0, 0),
|
||||
EK::Thief => s("unit.thief", 60.0, 6.0, 1.0, 3.1, 0.9, 50, 120),
|
||||
EK::Monk => s("unit.monk", 150.0, 12.0, 1.0, 2.3, 0.7, 80, 180),
|
||||
EK::Ranger => s("unit.ranger", 90.0, 11.0, 5.0, 2.2, 1.4, 80, 160),
|
||||
EK::Inquisitor => s("unit.inquisitor", 70.0, 7.0, 3.5, 1.8, 1.6, 90, 250),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +303,12 @@ pub struct Ent {
|
||||
#[serde(default)]
|
||||
pub plague: f32, // spell: seconds of plague left (poison that spreads)
|
||||
#[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)
|
||||
@@ -371,6 +398,9 @@ impl Ent {
|
||||
shield: 0.0,
|
||||
invis: 0.0,
|
||||
plague: 0.0,
|
||||
name: 0,
|
||||
kills: 0,
|
||||
bane: 0,
|
||||
ab_cd: 0.0,
|
||||
mood_t: 0.0,
|
||||
guard: false,
|
||||
@@ -513,6 +543,8 @@ pub struct MasterState {
|
||||
pub rune_next: u32,
|
||||
pub portal_timer: f32,
|
||||
pub punch_ready: f32,
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -624,10 +656,20 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub bridgework: crate::bridges::BridgeWork, // burning and worn bridges
|
||||
#[serde(default)]
|
||||
pub weather: crate::weather::Weather, // above ground (weather.rs)
|
||||
#[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
|
||||
#[serde(default)]
|
||||
pub snares: Vec<usize>, // tiles with a ranger's snare (rogues.rs)
|
||||
#[serde(default)]
|
||||
pub talents: crate::talents::Talents, // of the master being processed (talents.rs)
|
||||
#[serde(default)]
|
||||
pub guard_marks: Vec<usize>, // guard markers (shields) of the master being processed (orders.rs)
|
||||
#[serde(default)]
|
||||
pub attack_marks: Vec<usize>, // attack markers (swords)
|
||||
@@ -975,8 +1017,13 @@ impl Game {
|
||||
vrng: crate::particles::vrng(),
|
||||
dwarf_caves: Vec::new(),
|
||||
bridgework: Default::default(),
|
||||
weather: Default::default(),
|
||||
memorials: Vec::new(),
|
||||
hall: Vec::new(),
|
||||
trading: Default::default(),
|
||||
happenings: None,
|
||||
snares: Vec::new(),
|
||||
talents: Default::default(),
|
||||
guard_marks: Vec::new(),
|
||||
attack_marks: Vec::new(),
|
||||
marks_t: 0.0,
|
||||
@@ -1205,7 +1252,7 @@ impl Game {
|
||||
|
||||
// what the next payday costs: wages of all creatures plus the training bill
|
||||
pub fn next_payday_cost(&self) -> f32 {
|
||||
let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum::<f32>() * self.talents.wages();
|
||||
wages + self.training_bill
|
||||
}
|
||||
|
||||
@@ -1331,6 +1378,9 @@ impl Game {
|
||||
e.disp_x = x;
|
||||
e.disp_y = y;
|
||||
e.level = level;
|
||||
if !hero && !matches!(kind, EK::Ram | EK::Catapult | EK::Trader) {
|
||||
e.name = crate::names::seed_for(id);
|
||||
}
|
||||
e.max_hp = s.hp * (1.0 + 0.15 * (level - 1) as f32);
|
||||
e.hp = e.max_hp;
|
||||
e.alive = true;
|
||||
@@ -1501,6 +1551,7 @@ impl Game {
|
||||
rune_next: self.rune_next,
|
||||
portal_timer: self.portal_timer,
|
||||
punch_ready: self.punch_ready,
|
||||
talents: self.talents.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,6 +1587,7 @@ impl Game {
|
||||
self.rune_next = m.rune_next;
|
||||
self.portal_timer = m.portal_timer;
|
||||
self.punch_ready = m.punch_ready;
|
||||
self.talents = m.talents;
|
||||
}
|
||||
|
||||
// make `side` the master being processed (no-op for sides that are no masters)
|
||||
@@ -1597,6 +1649,8 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
self.carve_master_area(*side, cores[k]);
|
||||
// every master gets a portal of his own (side 0 has the map's portal)
|
||||
self.add_master_portal(*side, cores[k]);
|
||||
self.add_master(*side, cores[k]);
|
||||
self.switch_to(*side);
|
||||
let (cx, cy) = cores[k];
|
||||
@@ -1610,6 +1664,30 @@ impl Game {
|
||||
true
|
||||
}
|
||||
|
||||
// a master's own portal right beside his starting area (if there is room)
|
||||
pub(crate) fn add_master_portal(&mut self, side: u8, core: (i32, i32)) {
|
||||
let owner = Owner::of_side(side);
|
||||
let (px, py) = (core.0 + 6, core.1);
|
||||
if px + 3 >= mw() as i32 - 2 || py < 2 || py + 3 >= mh() as i32 - 2 {
|
||||
return;
|
||||
}
|
||||
for y in py..py + 3 {
|
||||
for x in px..px + 3 {
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if !matches!(t.kind, Kind::Core | Kind::Gate) {
|
||||
t.kind = Kind::Portal;
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
t.marked = false;
|
||||
t.reinforced = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.map.portals.contains(&(px, py)) {
|
||||
self.map.portals.push((px, py));
|
||||
}
|
||||
}
|
||||
|
||||
// the starting area of a master: a 9x9 room of his own floor and his 3x3 soul core
|
||||
pub(crate) fn carve_master_area(&mut self, side: u8, core: (i32, i32)) {
|
||||
let owner = Owner::of_side(side);
|
||||
@@ -1695,7 +1773,7 @@ impl Game {
|
||||
// a master's household: mana, core, portals, payday
|
||||
fn economy(&mut self, dt: f32) {
|
||||
self.recount();
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max());
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * self.talents.mana_rate() * dt).min(self.mana_max());
|
||||
self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP);
|
||||
|
||||
let owned = self.portals_owned();
|
||||
@@ -1728,6 +1806,7 @@ impl Game {
|
||||
if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() {
|
||||
self.reinforce_corners();
|
||||
self.tidy_torch_stands();
|
||||
self.tidy_memorials();
|
||||
}
|
||||
self.update_forge(dt);
|
||||
self.update_research();
|
||||
@@ -1751,6 +1830,7 @@ impl Game {
|
||||
self.update_contested();
|
||||
self.update_units(dt);
|
||||
self.switch_to(first); // update_units worked master by master
|
||||
self.update_snares();
|
||||
self.update_towers(dt);
|
||||
self.update_reveals(dt);
|
||||
self.update_graves(dt);
|
||||
@@ -1758,6 +1838,7 @@ impl Game {
|
||||
self.update_rivals(dt);
|
||||
}
|
||||
self.update_dwarves(dt);
|
||||
self.update_weather(dt);
|
||||
self.update_bridges(dt);
|
||||
self.update_wood(dt);
|
||||
self.update_events(dt);
|
||||
@@ -1899,7 +1980,14 @@ impl Game {
|
||||
pub fn free_lair_aerie(&self) -> (usize, usize) {
|
||||
let flyers = self.ents.iter().filter(|e| e.alive && e.creature() && flies(e.kind)).count();
|
||||
let walkers = self.creature_count() - flyers;
|
||||
(self.counts[1].min(30).saturating_sub(walkers), self.counts[7].min(20).saturating_sub(flyers))
|
||||
// a bed per lair tile (aerie: per aerie tile), never more than MAX_CREATURES in all
|
||||
let room = MAX_CREATURES.saturating_sub(walkers + flyers);
|
||||
(self.counts[1].saturating_sub(walkers).min(room), self.counts[7].saturating_sub(flyers).min(room))
|
||||
}
|
||||
|
||||
// how many creatures this master can house: his beds, at most MAX_CREATURES
|
||||
pub fn creature_cap(&self) -> usize {
|
||||
(self.counts[1] + self.counts[7]).min(MAX_CREATURES)
|
||||
}
|
||||
|
||||
pub fn lair_factor(&self) -> f32 {
|
||||
@@ -1929,6 +2017,10 @@ impl Game {
|
||||
self.hint(1, "hint.no_farm");
|
||||
return;
|
||||
}
|
||||
if self.creature_count() >= MAX_CREATURES {
|
||||
self.hint(35, "hint.creature_max");
|
||||
return;
|
||||
}
|
||||
let (free_lair, free_aerie) = self.free_lair_aerie();
|
||||
if free_lair == 0 && free_aerie == 0 {
|
||||
self.hint(2, "hint.lair_full");
|
||||
@@ -2030,13 +2122,14 @@ impl Game {
|
||||
self.training_bill = 0.0;
|
||||
let mut n = 0;
|
||||
let mut total = bill;
|
||||
let merchant = self.talents.wages();
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || !self.ents[i].creature() {
|
||||
continue;
|
||||
}
|
||||
let r = self.rng.f();
|
||||
let e = &mut self.ents[i];
|
||||
let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32);
|
||||
let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32) * merchant;
|
||||
e.pay_due += w;
|
||||
e.pay_t = r * 55.0;
|
||||
n += 1;
|
||||
@@ -2104,7 +2197,7 @@ impl Game {
|
||||
if e.timer <= 0.0 {
|
||||
let cur = e.tile();
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0;
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0 && self.map.t[i].owner == self.me;
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
match found {
|
||||
Some(p) => {
|
||||
@@ -2225,6 +2318,12 @@ impl Game {
|
||||
if w >= 6 {
|
||||
pool.push((Giant, 1));
|
||||
}
|
||||
// the rogues of the later waves (in the campaign only from the later missions on)
|
||||
for (k, from_wave, from_mission) in crate::rogues::ROGUES {
|
||||
if w >= from_wave && self.mission.as_ref().map_or(true, |m| m.index >= from_mission) {
|
||||
pool.push((k, 1));
|
||||
}
|
||||
}
|
||||
// siege engines for the dungeon's doors, walls and towers
|
||||
if w >= 4 && self.traps.iter().any(|t| t.kind.door()) {
|
||||
pool.push((Ram, 1));
|
||||
@@ -2413,7 +2512,7 @@ impl Game {
|
||||
self.sfx(Sfx::LevelUp, e.x, e.y);
|
||||
}
|
||||
if !e.hero && e.level >= 3 {
|
||||
self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]);
|
||||
self.msg("msg.level_up", vec![crate::names::arg(e), e.level.to_string()]);
|
||||
}
|
||||
}
|
||||
if e.chained {
|
||||
@@ -2571,7 +2670,12 @@ impl Game {
|
||||
pub fn dig_out(&mut self, i: usize) {
|
||||
self.torches.retain(|t| t.wall != i);
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Floor;
|
||||
// a grove's mushroom leaves its moss behind
|
||||
let grove = t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0;
|
||||
t.kind = if grove { Kind::Grove } else { Kind::Floor };
|
||||
if grove {
|
||||
t.res = 0;
|
||||
}
|
||||
t.owner = Owner::None;
|
||||
t.marked = false;
|
||||
t.reinforced = false;
|
||||
@@ -2698,8 +2802,15 @@ 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) {
|
||||
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3);
|
||||
// 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 };
|
||||
let dmg = e.atk() * (0.85 + self.rng.f() * 0.3) * boost;
|
||||
let (tx, ty) = (self.ents[j].x, self.ents[j].y);
|
||||
let s = st(e.kind);
|
||||
// the higher the level, the bigger the attack looks (up to almost twice at Lv 10)
|
||||
@@ -2711,6 +2822,7 @@ impl Game {
|
||||
EK::Priestess => 0xff3080ff,
|
||||
EK::Owl => 0xffffa060,
|
||||
EK::Wyrm => 0xff2080ff,
|
||||
EK::Inquisitor => 0xff40d0ff,
|
||||
_ => 0xff70b0d0,
|
||||
};
|
||||
// flyers shoot from the air
|
||||
@@ -2719,7 +2831,8 @@ impl Game {
|
||||
self.grow_last_fx(lv);
|
||||
use crate::particles::*;
|
||||
self.style_last_fx(match e.kind {
|
||||
EK::Archer => ST_ARROW,
|
||||
EK::Archer | EK::Ranger => ST_ARROW,
|
||||
EK::Inquisitor => ST_HOLY,
|
||||
EK::Mage => ST_SHADOW,
|
||||
EK::Priest => ST_HOLY,
|
||||
EK::Priestess | EK::Wyrm | EK::BattleMage => ST_FIRE,
|
||||
@@ -2806,9 +2919,17 @@ impl Game {
|
||||
}
|
||||
let my_id = e.id;
|
||||
e.invis = 0.0; // attacking breaks invisibility
|
||||
let armour = {
|
||||
let t = &self.ents[j];
|
||||
if !t.hero && Owner::MASTER_SIDES.contains(&t.side) { self.talents_of(t.side).armour() } else { 1.0 }
|
||||
};
|
||||
let t = &mut self.ents[j];
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg };
|
||||
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();
|
||||
@@ -2866,6 +2987,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
self.release_all(id);
|
||||
// a named creature of some standing gets a memorial stone
|
||||
let dead = self.ents[j].clone();
|
||||
let remembered = self.raise_memorial(&dead);
|
||||
self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070);
|
||||
if kind != EK::Worker && kind != EK::Revenant {
|
||||
self.bury(x, y);
|
||||
@@ -2881,7 +3005,8 @@ impl Game {
|
||||
self.switch_to(me);
|
||||
}
|
||||
if hero {
|
||||
let b = s.bounty as f32;
|
||||
let b = s.bounty as f32 + if kind == EK::Thief { self.thief_loot(id) } else { 0.0 };
|
||||
self.add_master_xp(s.bounty as f32 * crate::talents::HERO_XP);
|
||||
let got = self.add_gold(b);
|
||||
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
|
||||
self.sfx(Sfx::HeroDeath, x, y);
|
||||
@@ -2897,7 +3022,9 @@ impl Game {
|
||||
self.msg("msg.rival_unit_slain", vec![format!("@{}", s.name), (got as i32).to_string()]);
|
||||
} else {
|
||||
self.sfx(Sfx::Death, x, y);
|
||||
self.msg("msg.creature_died", vec![format!("@{}", s.name)]);
|
||||
if !remembered {
|
||||
self.msg("msg.creature_died", vec![crate::names::arg(&dead)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2918,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);
|
||||
@@ -3269,12 +3403,13 @@ impl Game {
|
||||
|
||||
// ---------------------------------------------------------------- pit imps (workers)
|
||||
|
||||
fn dig_neighbor(&self, i: usize, me: u32) -> Option<usize> {
|
||||
// a neighbour of tile i that this imp may dig: marked by its own master (side)
|
||||
fn dig_neighbor(&self, i: usize, me: u32, side: u8) -> Option<usize> {
|
||||
let (x, y) = xy(i);
|
||||
let mut found = None;
|
||||
for &(dx, dy) in &DIR4 {
|
||||
if let Some(t) = self.map.get(x + dx, y + dy) {
|
||||
if t.marked && t.diggable() {
|
||||
if t.marked && t.mark_by == side && t.diggable() {
|
||||
if t.reserved == me {
|
||||
return Some(idx(x + dx, y + dy));
|
||||
}
|
||||
@@ -3312,14 +3447,16 @@ impl Game {
|
||||
None
|
||||
}
|
||||
|
||||
fn claimable(&self, i: usize, me: u32) -> bool {
|
||||
pub(crate) fn claimable(&self, i: usize, me: u32) -> bool {
|
||||
let t = &self.map.t[i];
|
||||
let ok = if t.owner == self.me {
|
||||
false
|
||||
} else if t.owner == Owner::None {
|
||||
matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest)
|
||||
} else if t.owner != Owner::Hero && self.allied(self.me_side, t.owner.side()) {
|
||||
false // an ally's ground is never taken
|
||||
} else {
|
||||
// knight and other masters' ground (also vaults and shard chambers), but not while enemies fight there
|
||||
// knight and enemy masters' ground (also vaults and shard chambers), but not while enemies fight there
|
||||
matches!(t.kind, Kind::Floor | Kind::Treasury | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Lair | Kind::Training) && !self.contested.get(i).copied().unwrap_or(false)
|
||||
};
|
||||
if !ok {
|
||||
@@ -3376,7 +3513,7 @@ impl Game {
|
||||
let me = self.me;
|
||||
let target = e
|
||||
.bed
|
||||
.filter(|&b| self.map.t[b].kind == bed_kind)
|
||||
.filter(|&b| self.map.t[b].kind == bed_kind && self.map.t[b].owner == me)
|
||||
.or_else(|| self.map.search(cur, self.map.creature_cost(fly), |i| self.map.t[i].kind == bed_kind && self.map.t[i].owner == me, true).map(|p| p.last().copied().unwrap_or(cur)))
|
||||
.or_else(|| self.map.search(cur, self.map.creature_cost(fly), |i| self.map.adj_core_of(i, me), true).map(|p| p.last().copied().unwrap_or(cur)));
|
||||
let Some(b) = target else { return false };
|
||||
@@ -3412,7 +3549,7 @@ impl Game {
|
||||
match e.job {
|
||||
Job::Dig(t) => {
|
||||
let tile = self.map.t[t];
|
||||
if !tile.marked || !tile.diggable() || !adj4(cur, t) {
|
||||
if !tile.marked || tile.mark_by != e.side || !tile.diggable() || !adj4(cur, t) {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
return;
|
||||
@@ -3428,7 +3565,7 @@ impl Game {
|
||||
e.face = sgn(tx as f32 + 0.5 - e.x);
|
||||
e.working = true;
|
||||
let skilled = if crate::personality::knows(e, "dig") { 1.25 } else { 1.0 };
|
||||
e.timer += dt * self.help_speed(t) * skilled;
|
||||
e.timer += dt * self.help_speed(t) * skilled * self.talents.dig_speed();
|
||||
if e.timer >= 0.45 {
|
||||
e.timer = 0.0;
|
||||
let fxx = (e.x + tx as f32 + 0.5) * 0.5;
|
||||
@@ -3588,7 +3725,7 @@ impl Game {
|
||||
}
|
||||
Job::Deliver => {
|
||||
if self.is_deposit(cur) {
|
||||
let amount = e.carry as f32;
|
||||
let amount = e.carry as f32 * self.talents.treasure();
|
||||
let got = self.add_res_at(amount, e.carry_res, Some(cur));
|
||||
if got < amount {
|
||||
self.hint(3, "hint.treasury_full");
|
||||
@@ -3647,9 +3784,9 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) {
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me, e.side).is_some(), true) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
if let Some(t) = self.dig_neighbor(end, me) {
|
||||
if let Some(t) = self.dig_neighbor(end, me, e.side) {
|
||||
self.map.t[t].reserved = me;
|
||||
e.path = p;
|
||||
e.job = Job::Dig(t);
|
||||
@@ -3795,7 +3932,7 @@ impl Game {
|
||||
let beds = if flies(e.kind) { self.counts[7] } else { self.counts[1] };
|
||||
if e.bed.is_none() && beds > 0 && !matches!(e.job, Job::Attack(_) | Job::Leave | Job::Rally | Job::March(_)) && self.rng.f() < 0.15 {
|
||||
let cur = e.tile();
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0;
|
||||
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0 && self.map.t[i].owner == self.me;
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
let b = p.last().copied().unwrap_or(cur);
|
||||
@@ -4215,7 +4352,9 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn add_mark(&mut self, x: f32, y: f32, kind: u8, side: u8, duration: f32, fixed: bool) {
|
||||
if (!fixed && !self.debug_markers) || duration <= 0.0 || !x.is_finite() || !y.is_finite() {
|
||||
// the shields, swords and the flag show themselves; these markers are for debugging only
|
||||
let _ = fixed;
|
||||
if !self.debug_markers || duration <= 0.0 || !x.is_finite() || !y.is_finite() {
|
||||
return;
|
||||
}
|
||||
let until = self.sim_steps + (duration / STEP).ceil() as u64;
|
||||
@@ -4243,14 +4382,18 @@ impl Game {
|
||||
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
|
||||
if let Some(b) = e.bed {
|
||||
let t = &self.map.t[b];
|
||||
if t.kind != bed_kind || t.bed != me {
|
||||
// a bed in somebody else's lair (an ally's, or ground that changed hands) is given up
|
||||
if t.kind != bed_kind || t.bed != me || t.owner != self.me {
|
||||
if t.bed == me {
|
||||
self.map.t[b].bed = 0;
|
||||
}
|
||||
e.bed = None;
|
||||
}
|
||||
}
|
||||
if e.bed.is_none() {
|
||||
let free = |i: usize| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == bed_kind && t.bed == 0
|
||||
t.kind == bed_kind && t.bed == 0 && t.owner == self.me
|
||||
};
|
||||
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
|
||||
if let Some(p) = found {
|
||||
@@ -4581,6 +4724,10 @@ impl Game {
|
||||
if e.boss > 0 {
|
||||
self.boss_ability(e, dt);
|
||||
}
|
||||
// the rogues' tricks: a thief who is busy stealing or running does nothing else
|
||||
if self.rogue_ai(e, dt) {
|
||||
return;
|
||||
}
|
||||
if e.kind == EK::Fairy {
|
||||
// fairy dust: the heroes around are healed a little all the time
|
||||
for o in self.ents.iter_mut() {
|
||||
@@ -4756,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 {
|
||||
@@ -4763,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;
|
||||
}
|
||||
@@ -4828,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);
|
||||
@@ -4851,9 +5020,11 @@ impl Game {
|
||||
for x in x0.min(x1)..=x0.max(x1) {
|
||||
if inb(x, y) {
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if t.diggable() {
|
||||
// a master sets and clears only his own marks
|
||||
let side = self.me.side().min(7);
|
||||
if t.diggable() && (!t.marked || t.mark_by == side) {
|
||||
t.marked = on;
|
||||
t.mark_by = self.me.side().min(7);
|
||||
t.mark_by = side;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4862,7 +5033,7 @@ impl Game {
|
||||
|
||||
pub fn build_rect(&mut self, room: usize, x0: i32, y0: i32, x1: i32, y1: i32) {
|
||||
let kind = ROOM_KINDS[room];
|
||||
let cost = ROOM_COST[room] as f32;
|
||||
let cost = (ROOM_COST[room] as f32 * self.talents.room_cost()).round();
|
||||
let mut built = 0;
|
||||
let mut broke = false;
|
||||
for y in y0.min(y1)..=y0.max(y1) {
|
||||
@@ -5616,20 +5787,20 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn lightning(&mut self, wx: f32, wy: f32) {
|
||||
const COST: f32 = 50.0;
|
||||
if self.mana < COST {
|
||||
let cost = 50.0 * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_bolt", vec![]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.mana -= COST;
|
||||
self.mana -= cost;
|
||||
self.count(crate::stats::SPELLS, 1.0);
|
||||
self.sfx(Sfx::Lightning, wx, wy);
|
||||
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||
self.ignite_at(wx, wy);
|
||||
let mut kills = Vec::new();
|
||||
for (j, e) in self.ents.iter_mut().enumerate() {
|
||||
if e.alive && !e.held && e.active_hero() && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
|
||||
if e.alive && !e.held && e.active_hero() && !crate::rogues::spell_proof(e) && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
|
||||
e.hp -= 55.0;
|
||||
e.flash = 0.2;
|
||||
if e.hp <= 0.0 {
|
||||
@@ -5643,13 +5814,13 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn heal(&mut self, wx: f32, wy: f32) {
|
||||
const COST: f32 = 60.0;
|
||||
if self.mana < COST {
|
||||
let cost = 60.0 * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_heal", vec![]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.mana -= COST;
|
||||
self.mana -= cost;
|
||||
self.count(crate::stats::SPELLS, 1.0);
|
||||
self.sfx(Sfx::Heal, wx, wy);
|
||||
let mut fxs = Vec::new();
|
||||
@@ -6826,6 +6997,92 @@ mod tests {
|
||||
assert!(g.ents.iter().filter(|e| e.kind == EK::Chainbreaker).count() <= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_beds_decide_how_many_creatures_come_but_never_more_than_100() {
|
||||
let mut g = Game::new(9, 96, 96);
|
||||
let (cx, cy) = g.map.core;
|
||||
// a lair of 40 beds: room for 40
|
||||
let mut n = 0;
|
||||
for y in cy + 4..cy + 20 {
|
||||
for x in cx - 6..cx + 8 {
|
||||
if n < 40 && inb(x, y) {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lair;
|
||||
t.owner = Owner::Player;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.recount();
|
||||
assert_eq!(g.creature_cap(), 40);
|
||||
assert_eq!(g.free_lair_aerie().0, 40, "more than the old 30");
|
||||
// a huge lair: the cap is 100
|
||||
for y in cy + 4..cy + 20 {
|
||||
for x in cx - 10..cx + 12 {
|
||||
if inb(x, y) && g.map.t[idx(x, y)].kind != Kind::Core {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lair;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.recount();
|
||||
assert!(g.counts[1] > 100);
|
||||
assert_eq!(g.creature_cap(), MAX_CREATURES);
|
||||
for k in 0..100 {
|
||||
g.spawn(EK::Kobold, false, cx as f32 + 0.5 + (k % 10) as f32, cy as f32 + 5.5 + (k / 10) as f32, 1);
|
||||
}
|
||||
assert_eq!(g.free_lair_aerie(), (0, 0), "no room for a 101st");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creatures_never_make_their_bed_in_an_allys_lair() {
|
||||
let mut g = Game::new(11, 96, 128);
|
||||
g.coop = true;
|
||||
assert!(g.start_masters(&[0, 2], 0));
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy + 4..cy + 7 {
|
||||
for x in cx - 1..cx + 4 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Lair;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
g.recount();
|
||||
let id = g.spawn(EK::Kobold, false, cx as f32 + 1.5, cy as f32 + 5.5, 1);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.side = 2;
|
||||
e.energy = 0.1;
|
||||
}
|
||||
for _ in 0..(40 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
let e = g.ents.iter().find(|e| e.id == id).expect("still there");
|
||||
assert!(e.bed.map_or(true, |b| g.map.t[b].owner != Owner::Player), "no bed in the ally's lair");
|
||||
assert!(!g.map.t.iter().any(|t| t.kind == Kind::Lair && t.bed == id + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_master_of_a_coop_mission_has_an_own_portal() {
|
||||
let m = crate::campaign::coop_mission_indexes()[2];
|
||||
let mut g = crate::campaign::start_mission(m, None);
|
||||
g.coop = true;
|
||||
assert!(g.start_masters(&[0, 2, 3], 0));
|
||||
for side in [2u8, 3] {
|
||||
let owner = Owner::of_side(side);
|
||||
let core = g.masters.iter().find(|m| m.side == side).unwrap().core;
|
||||
let k = (0..g.map.portals.len()).find(|&k| g.map.portal_owner(k) == owner && g.map.portals[k] == (core.0 + 6, core.1)).expect("a portal of his own");
|
||||
let (px, py) = g.map.portals[k];
|
||||
let from = idx(core.0 + 1, core.1 + 4);
|
||||
assert!(g.map.creature_path(false, from, idx(px, py + 1)).is_some(), "reachable from his dungeon");
|
||||
}
|
||||
for n in crate::lobby::MATCH_SIZES {
|
||||
let seats = crate::lobby::seats_for(n);
|
||||
let mut m = Game::new(3, n, n);
|
||||
assert!(m.start_masters(seats, 0), "a {}x{} match with {} seats", n, n, seats.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_coop_ai_seat_fights_beside_the_host_not_against_him() {
|
||||
let mut g = Game::new(11, 96, 128);
|
||||
|
||||
+80
-3
@@ -93,11 +93,13 @@ pub const I_RAM: u32 = 209; // battering ram tool
|
||||
pub const I_CATAPULT: u32 = 210; // catapult tool
|
||||
pub const T_SNOW: u32 = 211; // 4 variants: snow on the land above ground (winter)
|
||||
pub const T_PUMPKIN: u32 = 215; // a carved pumpkin (Halloween)
|
||||
pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glowing spores
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
pub sprites: Texture2D,
|
||||
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
|
||||
pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs)
|
||||
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
|
||||
pub font_map: FontMap, // which glyphs the two fonts hold
|
||||
}
|
||||
@@ -943,6 +945,22 @@ fn gen_atlas() -> Image {
|
||||
Some(if r % 11 == 0 { c(176, 200, 96) } else if r % 6 == 0 { c(112, 150, 62) } else if r % 23 == 0 { c(236, 220, 110) } else if r % 37 == 0 { c(230, 236, 240) } else { c(140, 176, 78) })
|
||||
});
|
||||
}
|
||||
// mushroom grove: dark stones overgrown with teal moss, a few glowing spores
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_MOSS + v, |x, y| {
|
||||
let r = hash(x, y, 520 + v);
|
||||
let moss = (hash(x / 3, y / 3, 530 + v) % 5) < 3;
|
||||
Some(if r % 41 == 0 {
|
||||
c(150, 255, 220) // a spore glows
|
||||
} else if r % 29 == 0 {
|
||||
c(200, 150, 255)
|
||||
} else if moss {
|
||||
if r % 4 == 0 { c(46, 112, 96) } else if r % 3 == 0 { c(30, 84, 74) } else { c(38, 98, 84) }
|
||||
} else {
|
||||
stones(x, y, v, (52, 58, 56), (26, 30, 30), 8)
|
||||
})
|
||||
});
|
||||
}
|
||||
// obsidian: cooled lava, dark glass with a few glowing seams
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_OBSIDIAN + v, |x, y| {
|
||||
@@ -1437,6 +1455,58 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
|
||||
"..kBkkBk..",
|
||||
"..kk..kk..",
|
||||
],
|
||||
// thief: a dark hood and mask, a dagger, a sack of gold on the back
|
||||
EK::Thief => [
|
||||
"...kkkk...",
|
||||
"..kddddk..",
|
||||
"..kdfkfk.Y",
|
||||
"..kddddkYy",
|
||||
".kdddddkYy",
|
||||
"skdddddk..",
|
||||
".kdkdddk..",
|
||||
"..kddddk..",
|
||||
"..kdkkdk..",
|
||||
"..kk..kk..",
|
||||
],
|
||||
// monk: shaven head, an orange robe tied with a rope, bare fists
|
||||
EK::Monk => [
|
||||
"...kkkk...",
|
||||
"..kffffk..",
|
||||
"..kfkfkk..",
|
||||
"..kffffk..",
|
||||
".kooooook.",
|
||||
"kfoooooofk",
|
||||
".kooyyook.",
|
||||
".kOooooOk.",
|
||||
".kOooooOk.",
|
||||
"..kkkkkk..",
|
||||
],
|
||||
// ranger: a green hood and cloak, a long bow, leather boots
|
||||
EK::Ranger => [
|
||||
"...kkkk.b.",
|
||||
"..kgGGgkkb",
|
||||
"..kGfkfk.b",
|
||||
"..kGfffk.b",
|
||||
".kGgggggkb",
|
||||
".kGgbbggkb",
|
||||
".kGggggk.b",
|
||||
"..kBBBBk.b",
|
||||
"..kBkkBkb.",
|
||||
"..kk..kk..",
|
||||
],
|
||||
// inquisitor: a tall red hat, white and red robes, a glowing holy book
|
||||
EK::Inquisitor => [
|
||||
"...kRRk...",
|
||||
"..kRrrRk..",
|
||||
"..kRrrRk..",
|
||||
"..kffffk..",
|
||||
"..kfkfkk..",
|
||||
".kwwrrwwk.",
|
||||
"kwkwrrwkek",
|
||||
".kwwrrwwkk",
|
||||
".kRwwwwRk.",
|
||||
"..kkkkkk..",
|
||||
],
|
||||
EK::Priest => [
|
||||
"....yy....",
|
||||
"...kkkk...",
|
||||
@@ -1721,7 +1791,7 @@ fn pal(ch: char) -> Option<Color> {
|
||||
})
|
||||
}
|
||||
|
||||
pub const SPRITE_KINDS: [EK; 27] = [
|
||||
pub const SPRITE_KINDS: [EK; 31] = [
|
||||
EK::Worker,
|
||||
EK::Kobold,
|
||||
EK::Spider,
|
||||
@@ -1749,6 +1819,10 @@ pub const SPRITE_KINDS: [EK; 27] = [
|
||||
EK::CaveBear,
|
||||
EK::Beetle,
|
||||
EK::Trader,
|
||||
EK::Thief,
|
||||
EK::Monk,
|
||||
EK::Ranger,
|
||||
EK::Inquisitor,
|
||||
];
|
||||
// width of the sprite texture in pixels
|
||||
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
|
||||
@@ -1766,7 +1840,7 @@ pub fn gear_tier(level: u32) -> u32 {
|
||||
}
|
||||
|
||||
fn is_hero_kind(k: EK) -> bool {
|
||||
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage)
|
||||
matches!(k, EK::Knight | EK::Archer | EK::Priest | EK::Dwarf | EK::Paladin | EK::Giant | EK::Fairy | EK::BattleMage | EK::Thief | EK::Monk | EK::Ranger | EK::Inquisitor)
|
||||
}
|
||||
|
||||
// Views of a sprite for eight directions (the others are mirrored): 0 front, 1 front
|
||||
@@ -2089,7 +2163,9 @@ impl Gfx {
|
||||
font.set_filter(FilterMode::Nearest);
|
||||
let cursors = Texture2D::from_image(&gen_cursors());
|
||||
cursors.set_filter(FilterMode::Nearest);
|
||||
Gfx { atlas, sprites, cursors, font, font_map }
|
||||
let badges = Texture2D::from_image(&crate::badges::gen_badges());
|
||||
badges.set_filter(FilterMode::Nearest);
|
||||
Gfx { atlas, sprites, cursors, badges, font, font_map }
|
||||
}
|
||||
|
||||
pub fn cell(id: u32) -> Rect {
|
||||
@@ -2159,6 +2235,7 @@ impl Gfx {
|
||||
Kind::Obsidian => T_OBSIDIAN + v,
|
||||
Kind::Mushroom => T_FLOOR + v,
|
||||
Kind::Tree => T_GRASS + v,
|
||||
Kind::Grove => T_MOSS + v,
|
||||
Kind::LavaBridge => T_STONEBRIDGE + (v & 1),
|
||||
Kind::Shard => T_HEROFLOOR + v,
|
||||
Kind::Aerie => T_AERIE + v,
|
||||
|
||||
+7
-1
@@ -97,7 +97,13 @@ pub fn tr(key: &str) -> String {
|
||||
pub fn trf(key: &str, args: &[String]) -> String {
|
||||
let mut s = tr(key);
|
||||
for (n, a) in args.iter().enumerate() {
|
||||
let v = if let Some(k) = a.strip_prefix('@') { tr(k) } else { a.clone() };
|
||||
let v = if let Some(k) = a.strip_prefix('@') {
|
||||
tr(k)
|
||||
} else if let Some(n) = a.strip_prefix('%') {
|
||||
crate::names::from_arg(n) // a creature's name (names.rs)
|
||||
} else {
|
||||
a.clone()
|
||||
};
|
||||
s = s.replace(&format!("{{{}}}", n), &v);
|
||||
}
|
||||
s
|
||||
|
||||
@@ -673,6 +673,17 @@ fn teams_from(sides: &[u8], v: Option<&Value>) -> Vec<(u8, u8)> {
|
||||
sides.iter().copied().zip(teams).collect()
|
||||
}
|
||||
|
||||
// versus map sizes and how many master seats each one has
|
||||
pub const MATCH_SIZES: [usize; 7] = [80, 96, 128, 160, 192, 224, 256];
|
||||
|
||||
pub fn seats_for(size: usize) -> &'static [u8] {
|
||||
match size {
|
||||
0..=80 => &[0, 2],
|
||||
81..=96 => &[0, 2, 3],
|
||||
_ => &[0, 2, 3, 4],
|
||||
}
|
||||
}
|
||||
|
||||
fn seats_from(v: Option<&Value>) -> Vec<SeatInfo> {
|
||||
v.and_then(|x| x.as_array())
|
||||
.map(|arr| {
|
||||
|
||||
+67
-11
@@ -1,9 +1,12 @@
|
||||
// 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;
|
||||
mod armory;
|
||||
mod audio;
|
||||
mod badges;
|
||||
mod campaign;
|
||||
mod campaign_doc;
|
||||
mod cloud;
|
||||
@@ -28,6 +31,11 @@ mod particles;
|
||||
mod props;
|
||||
mod research;
|
||||
mod rival;
|
||||
mod rogues;
|
||||
mod balance;
|
||||
mod talents;
|
||||
mod weather;
|
||||
mod names;
|
||||
mod render3d;
|
||||
mod rooms;
|
||||
mod save;
|
||||
@@ -96,6 +104,7 @@ struct Input {
|
||||
rlast: (f32, f32),
|
||||
rdrag: bool,
|
||||
mlast: Option<(f32, f32)>, // middle button drag
|
||||
mclick: f64, // time of the last middle click (a double click resets the view)
|
||||
minimap_drag: bool,
|
||||
// touch
|
||||
tlast: Option<Vec2>, // last position of a one-finger pan (device pixels)
|
||||
@@ -135,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;
|
||||
@@ -358,7 +376,11 @@ fn account_action(a: MenuAction, cloud: &mut Cloud, menu: &mut Menu, app: &mut A
|
||||
}
|
||||
}
|
||||
MenuAction::MatchJoin(code) => app.client.join(&code),
|
||||
MenuAction::MatchCreate => app.client.create_land(&[0, 2, 3, 4], app.settings.map_w, app.settings.map_h, app.settings.difficulty, app.settings.surface, app.settings.events),
|
||||
MenuAction::MatchCreate => {
|
||||
// the map size decides the number of master seats
|
||||
let n = app.settings.match_size;
|
||||
app.client.create_land(lobby::seats_for(n), n, n, app.settings.difficulty, app.settings.surface, app.settings.events)
|
||||
}
|
||||
MenuAction::MatchCoopCreate(mission) => {
|
||||
let all = campaign::missions();
|
||||
if let Some(d) = all.get(mission) {
|
||||
@@ -898,7 +920,7 @@ async fn main() {
|
||||
let mut app = App { game, cam, settings, seed, status: None, recorded: false, client: MatchClient::new(), ws_id: 0, match_mode: false, coop: false, coop_editor: false, coop_run: None, offline: false, tutorial: false, fps: 0.0, renderer: platform::renderer(), frame_ms: 0.0, frame_hist: [0.0; FRAME_HIST], frame_hist_pos: 0, custom_campaign: None, custom_progress: None, custom_mission_index: None, custom_next_index: None, editor: None, editor_testing: false, editor_test_only: false, editor_public_pending: false, editor_loading: None, pending_briefing: None };
|
||||
menu.progress = platform::storage_get(PROGRESS_KEY).and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default();
|
||||
apply_game_settings(&mut app.game, &app.settings);
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
let mut inp = Input { ldown: None, rdown: None, rlast: (0.0, 0.0), rdrag: false, mlast: None, mclick: -1.0, minimap_drag: false, tlast: None, touch_world: false, inspect: None };
|
||||
let mut touch = TouchInput::new();
|
||||
let mut auto_touch = platform::coarse_pointer();
|
||||
let mut menu_swipe: Option<(f32, f32)> = None; // a swipe over a menu: last finger y, unused distance
|
||||
@@ -909,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();
|
||||
@@ -962,9 +988,19 @@ async fn main() {
|
||||
// pixel look: the interface on a virtual screen whose pixels are whole parts of the world's
|
||||
let rows = if [270, 360, 480].contains(&app.settings.pixel_rows) { app.settings.pixel_rows as u32 } else { 360 };
|
||||
// the interface needs room: the smallest whole multiple of the world's rows with at least
|
||||
// 900 rows on a desktop (360 -> 1080) and 540 on a touch screen (360 -> 720)
|
||||
let need: u32 = if touch_mode { 540 } else { 900 };
|
||||
ui::set_virtual_rows(rows * need.div_ceil(rows));
|
||||
// 900 rows (360 -> 1080), on a desktop and on a touch screen alike (a phone has about
|
||||
// 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. 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);
|
||||
if touch_mode {
|
||||
// the pointer is the finger, or the position of this frame's gesture
|
||||
let gpos = gestures.iter().rev().find_map(|g| match *g {
|
||||
@@ -1325,6 +1361,16 @@ async fn main() {
|
||||
if is_key_down(key(Action::TiltDown)) {
|
||||
cam.pitch -= rot * 0.6;
|
||||
}
|
||||
// a double click with the middle button: the standard angle and turn (same place)
|
||||
if is_mouse_button_pressed(MouseButton::Middle) && !touch_mode {
|
||||
let now = get_time();
|
||||
if now - inp.mclick < 0.35 {
|
||||
cam.reset_view();
|
||||
inp.mclick = -1.0;
|
||||
} else {
|
||||
inp.mclick = now;
|
||||
}
|
||||
}
|
||||
if is_mouse_button_down(MouseButton::Middle) && !touch_mode {
|
||||
if let Some((lx, ly)) = inp.mlast {
|
||||
let inv = if keys.invert_mouse { -1.0 } else { 1.0 };
|
||||
@@ -1385,7 +1431,7 @@ async fn main() {
|
||||
if !inp.rdrag && !over_ui && app.game.state == 0 {
|
||||
if inp.ldown.is_some() {
|
||||
inp.ldown = None;
|
||||
} else if app.game.hand.is_empty() && matches!(ui.tool, Tool::Order(_)) {
|
||||
} else if app.game.hand.is_empty() && matches!(ui.tool, Tool::Order(_) | Tool::Flag) {
|
||||
app.game.queue(Act::Unmark { x: wx, y: wy });
|
||||
} else {
|
||||
app.game.queue(Act::Drop { x: wx, y: wy });
|
||||
@@ -1457,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()) {
|
||||
@@ -1504,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) {
|
||||
@@ -1804,6 +1850,7 @@ app.pending_briefing = briefing_of(&campaign, i);
|
||||
}
|
||||
UiAction::OpenMenu => menu.page = Some(Page::Pause),
|
||||
UiAction::SendGold(to, amount) => app.game.queue(Act::SendGold { to, amount }),
|
||||
UiAction::LearnTalent(branch) => app.game.queue(Act::Talent { branch }),
|
||||
UiAction::Locked => {
|
||||
let locked = ui.last_locked.unwrap_or(usize::MAX);
|
||||
app.game.msg(if app.game.can_research(locked) { "msg.tool_research" } else { "msg.tool_locked" }, vec![]);
|
||||
@@ -1961,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 {
|
||||
@@ -2097,7 +2145,11 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
let ground = app.cam.ground_point(mx, my);
|
||||
clear_background(Color::from_rgba(4, 3, 5, 255));
|
||||
let (color, radius) = match ui.tool {
|
||||
Tool::Dig => (Color::new(1.0, 0.85, 0.3, 1.0), None),
|
||||
Tool::Dig => {
|
||||
// the dig selection in the master's colour (like the marks)
|
||||
let c = render3d::master_rgb(app.game.local_side);
|
||||
(Color::new(c.x, c.y, c.z, 1.0), None)
|
||||
}
|
||||
Tool::Hand => (Color::new(0.9, 0.75, 0.6, 1.0), None),
|
||||
Tool::Room(_) => (Color::new(0.4, 0.85, 1.0, 1.0), None),
|
||||
Tool::Sell => (Color::new(1.0, 0.5, 0.3, 1.0), None),
|
||||
@@ -2303,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() {
|
||||
|
||||
+17
-3
@@ -70,10 +70,14 @@ pub enum Kind {
|
||||
Altar, // room: sacrifices
|
||||
Drawbridge, // bridge that can be raised (res: see DB_RAISED, DB_AUTO, DB_LAVA)
|
||||
Obsidian, // lava cooled by frost: walkable until it melts again
|
||||
Mushroom, // giant mushroom on open ground: imps fell it for wood (grows back)
|
||||
Mushroom, // giant mushroom: imps fell it for wood once it is grown (food: growth left, 0 = grown; res: MUSH_GROVE)
|
||||
Tree, // tree above ground: imps fell it for wood
|
||||
Grove, // mushroom grove: mossy ground nobody can claim, giant mushrooms grow (back) here
|
||||
}
|
||||
|
||||
// a giant mushroom of a grove: when felled, the ground turns back into grove (Tile::res)
|
||||
pub const MUSH_GROVE: u8 = 1;
|
||||
|
||||
// drawbridge state in Tile::res
|
||||
pub const DB_RAISED: u8 = 1; // raised: nobody can cross
|
||||
pub const DB_AUTO: u8 = 2; // rises by itself when enemies come near
|
||||
@@ -105,7 +109,7 @@ pub struct River {
|
||||
}
|
||||
|
||||
// order matters for the packed savegame codec: only append new kinds
|
||||
const KINDS: [Kind; 38] = [
|
||||
const KINDS: [Kind; 39] = [
|
||||
Kind::Rock,
|
||||
Kind::Earth,
|
||||
Kind::Gold,
|
||||
@@ -144,6 +148,7 @@ const KINDS: [Kind; 38] = [
|
||||
Kind::Obsidian,
|
||||
Kind::Mushroom,
|
||||
Kind::Tree,
|
||||
Kind::Grove,
|
||||
];
|
||||
|
||||
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
||||
@@ -229,7 +234,15 @@ impl Tile {
|
||||
matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel)
|
||||
}
|
||||
pub fn diggable(&self) -> bool {
|
||||
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel | Kind::Mushroom | Kind::Tree)
|
||||
matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel | Kind::Tree) || (self.kind == Kind::Mushroom && self.grown())
|
||||
}
|
||||
// a giant mushroom is felled only when it is fully grown (everything else counts as grown)
|
||||
pub fn grown(&self) -> bool {
|
||||
self.kind != Kind::Mushroom || self.food <= 0.0
|
||||
}
|
||||
// how far a giant mushroom has grown: 0 a sprout .. 1 fully grown
|
||||
pub fn growth(&self) -> f32 {
|
||||
if self.kind == Kind::Mushroom { (1.0 - self.food).clamp(0.0, 1.0) } else { 1.0 }
|
||||
}
|
||||
// a giant mushroom or a tree: felled like a wall is dug, gives wood
|
||||
pub fn plant(&self) -> bool {
|
||||
@@ -272,6 +285,7 @@ impl Tile {
|
||||
| Kind::Arena
|
||||
| Kind::Altar
|
||||
| Kind::Obsidian
|
||||
| Kind::Grove
|
||||
) || (self.kind == Kind::Drawbridge && self.res & DB_RAISED == 0)
|
||||
}
|
||||
// river tiles (open, but nobody can walk there)
|
||||
|
||||
+38
-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,
|
||||
@@ -238,6 +237,7 @@ enum Btn {
|
||||
Tab(usize),
|
||||
Change(usize, i32),
|
||||
MapW(i32),
|
||||
MatchSize(i32),
|
||||
Land,
|
||||
MapH(i32),
|
||||
Difficulty(i32),
|
||||
@@ -303,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),
|
||||
@@ -437,6 +438,7 @@ pub struct Menu {
|
||||
pub error: Option<String>, // message on the login and account pages
|
||||
pub match_ui: MatchUi, // copied state of the multiplayer relay, shown on the Match page
|
||||
pub coop_mission: usize, // co-op campaign mission selected on the Match page
|
||||
match_size: usize, // the versus map size (from the settings, for drawing)
|
||||
pub coop_editor: bool, // the Match page offers a campaign document instead of the built-in missions
|
||||
pub coop_league: bool, // ... and that document is the co-op campaign "League against the Lightwatch"
|
||||
league_doc: Option<CampaignDoc>, // the co-op campaign, built the first time it is chosen
|
||||
@@ -453,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
|
||||
@@ -496,7 +499,9 @@ 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,
|
||||
coop_league: false,
|
||||
league_doc: None,
|
||||
@@ -682,6 +687,12 @@ impl Menu {
|
||||
MenuAction::Consumed
|
||||
}
|
||||
Some(Btn::Start) => MenuAction::NewGame,
|
||||
Some(Btn::MatchSize(d)) => {
|
||||
let sizes = crate::lobby::MATCH_SIZES;
|
||||
let k = sizes.iter().position(|&s| s >= settings.match_size).unwrap_or(2) as i32;
|
||||
settings.match_size = sizes[(k + d).clamp(0, sizes.len() as i32 - 1) as usize];
|
||||
MenuAction::SettingsChanged
|
||||
}
|
||||
Some(Btn::MapW(d)) => {
|
||||
settings.map_w = (settings.map_w as i32 + d * 32).clamp(MIN_SIZE as i32, MAX_SIZE as i32) as usize;
|
||||
MenuAction::SettingsChanged
|
||||
@@ -931,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;
|
||||
@@ -953,6 +969,7 @@ impl Menu {
|
||||
self.buttons.clear();
|
||||
self.lang_opts.clear();
|
||||
self.cur_lang = settings.language.clone();
|
||||
self.match_size = settings.match_size;
|
||||
let Some(page) = self.page else {
|
||||
self.lang_open = None;
|
||||
return;
|
||||
@@ -2053,6 +2070,19 @@ impl Menu {
|
||||
}
|
||||
}
|
||||
}
|
||||
// a new versus match: the map size and with it the number of seats
|
||||
if !ui.started && !ui.room_open() {
|
||||
let n = self.match_size;
|
||||
let seats = crate::lobby::seats_for(n).len();
|
||||
let r = Rect::new(ix, yy, 4.0 * CW(), CH() * 2.0);
|
||||
button(gfx, r, "<", r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchSize(-1)));
|
||||
gfx.text(&trf("net.map_size", &[n.to_string(), n.to_string(), seats.to_string()]), ix + 5.0 * CW(), yy + CH() * 0.5, S(), TXT);
|
||||
let r = Rect::new(ix + 30.0 * CW(), yy, 4.0 * CW(), CH() * 2.0);
|
||||
button(gfx, r, ">", r.contains(vec2(mx, my)), FRAME);
|
||||
self.buttons.push((r, Btn::MatchSize(1)));
|
||||
yy += CH() * 2.4;
|
||||
}
|
||||
self.field(gfx, ix, yy, w, &tr("net.join_hint"), 0, false);
|
||||
self.field(gfx, ix, yy + CH() * 2.4, w, &tr("net.chat"), 1, false);
|
||||
yy += CH() * 4.8;
|
||||
|
||||
@@ -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"))]);
|
||||
@@ -38,7 +44,7 @@ impl Menu {
|
||||
draw_rectangle(ix, ry, pic, pic, Color::new(0.08, 0.05, 0.05, 1.0));
|
||||
draw_rectangle_lines(ix, ry, pic, pic, 3.0, bc);
|
||||
let tint = if got { WHITE } else { Color::new(0.3, 0.3, 0.3, 1.0) };
|
||||
draw_texture_ex(&gfx.atlas, ix + 4.0, ry + 4.0, tint, DrawTextureParams { source: Some(crate::gfx::Gfx::cell(*icon)), dest_size: Some(vec2(pic - 8.0, pic - 8.0)), ..Default::default() });
|
||||
draw_texture_ex(&gfx.badges, ix + 4.0, ry + 4.0, tint, DrawTextureParams { source: Some(crate::badges::badge_rect(*icon)), dest_size: Some(vec2(pic - 8.0, pic - 8.0)), ..Default::default() });
|
||||
let tx = ix + pic + CW();
|
||||
let (name, desc) = if *hidden && !got { ("???".to_string(), tr("ach.hidden")) } else { texts(id) };
|
||||
gfx.text(&name, tx, ry + CH() * 0.1, S(), if got { GOLD } else { TXT });
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use crate::ui::{tool_cost_static, BLUE as BLUE_C, GREEN as GREEN_C, RED as RED_C
|
||||
pub const TABS: [&str; 4] = ["best.tab.creatures", "best.tab.humans", "best.tab.dungeon", "best.tab.ranks"];
|
||||
|
||||
const CREATURES: [EK; 16] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Jailer, EK::Priestess, EK::Chainbreaker, EK::Cogling, EK::Revenant, EK::Bat, EK::Gargoyle, EK::Owl, EK::Wyrm, EK::CaveBear, EK::Beetle];
|
||||
const HUMANS: [EK; 10] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::BattleMage, EK::Giant, EK::Paladin, EK::Ram, EK::Catapult];
|
||||
const HUMANS: [EK; 14] = [EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Fairy, EK::Thief, EK::BattleMage, EK::Monk, EK::Giant, EK::Ranger, EK::Inquisitor, EK::Paladin, EK::Ram, EK::Catapult];
|
||||
// buildings: indices into ui::TOOLS
|
||||
const BUILDINGS: [usize; 32] = [2, 3, 4, 5, 6, 12, 40, 13, 18, 21, 41, 42, 43, 44, 17, 47, 49, 19, 14, 15, 16, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
|
||||
|
||||
|
||||
+49
-1
@@ -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 ...")
|
||||
@@ -142,7 +144,15 @@ pub fn kind_by_name(name: &str) -> Option<EK> {
|
||||
18 => EK::Paladin,
|
||||
19 => EK::Giant,
|
||||
20 => EK::Fairy,
|
||||
_ => EK::BattleMage,
|
||||
21 => EK::BattleMage,
|
||||
22 => EK::Ram,
|
||||
23 => EK::Catapult,
|
||||
24 => EK::CaveBear,
|
||||
25 => EK::Beetle,
|
||||
26 => EK::Thief,
|
||||
27 => EK::Monk,
|
||||
28 => EK::Ranger,
|
||||
_ => EK::Inquisitor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -531,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
|
||||
@@ -560,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);
|
||||
@@ -816,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![]);
|
||||
@@ -846,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) {
|
||||
@@ -952,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);
|
||||
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
// Named creatures: every creature of a master carries a name seed from its birth. The name
|
||||
// comes from the list of its kind in the language files (so it follows a language switch);
|
||||
// from level 5 on it earns a humorous epithet, from level 8 a grand one. A creature of level
|
||||
// 3 or more that falls leaves a memorial stone with its name on its master's ground.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::i18n::{tr, trf};
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// 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 {
|
||||
pub tile: usize,
|
||||
pub kind: EK,
|
||||
pub name: u32,
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
// the name list of a kind of creature
|
||||
pub fn group(k: EK) -> &'static str {
|
||||
match k {
|
||||
EK::Worker => "imp",
|
||||
EK::Kobold => "kobold",
|
||||
EK::Spider => "spider",
|
||||
EK::Mage => "mage",
|
||||
EK::Ogre => "ogre",
|
||||
EK::Jailer => "jailer",
|
||||
EK::Priestess => "priestess",
|
||||
EK::Chainbreaker => "chainbreaker",
|
||||
EK::Bat => "bat",
|
||||
EK::Gargoyle => "gargoyle",
|
||||
EK::Owl => "owl",
|
||||
EK::Wyrm => "wyrm",
|
||||
EK::Cogling => "cogling",
|
||||
EK::Revenant => "revenant",
|
||||
EK::CaveBear | EK::Beetle => "beast",
|
||||
_ => "human",
|
||||
}
|
||||
}
|
||||
|
||||
// the seed of a new creature's name (from its id: the same on every computer of a match)
|
||||
pub fn seed_for(id: u32) -> u32 {
|
||||
(id.wrapping_mul(2654435761) ^ 0x5bd1_e995) | 1
|
||||
}
|
||||
|
||||
fn pick(key: &str, seed: u32) -> Option<String> {
|
||||
let list = tr(key);
|
||||
if list == key {
|
||||
return None; // no list in this language
|
||||
}
|
||||
let items: Vec<&str> = list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||
(!items.is_empty()).then(|| items[seed as usize % items.len()].to_string())
|
||||
}
|
||||
|
||||
// the name with its epithet (if the level has earned one); "" if the creature has no name
|
||||
pub fn full_name(kind: EK, seed: u32, level: u32) -> String {
|
||||
if seed == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let Some(base) = pick(&format!("names.{}", group(kind)), seed) else { return String::new() };
|
||||
let list = if level >= 8 { "epithets.high" } else { "epithets.mid" };
|
||||
match pick(list, seed >> 7).filter(|_| level >= 5) {
|
||||
Some(ep) => trf("name.epithet", &[base, ep]),
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
|
||||
// the name of a unit, or its kind's name if it has none
|
||||
pub fn display_name(e: &Ent) -> String {
|
||||
let n = full_name(e.kind, e.name, e.level);
|
||||
if n.is_empty() { tr(st(e.kind).name) } else { n }
|
||||
}
|
||||
|
||||
// a message argument that is turned into the name when the message is shown (i18n::trf)
|
||||
pub fn arg(e: &Ent) -> String {
|
||||
if e.name == 0 {
|
||||
return format!("@{}", st(e.kind).name);
|
||||
}
|
||||
format!("%{}.{}.{}", crate::gfx::sprite_index(e.kind), e.name, e.level)
|
||||
}
|
||||
|
||||
// the counterpart of `arg`
|
||||
pub fn from_arg(a: &str) -> String {
|
||||
let p: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
|
||||
match p.as_slice() {
|
||||
[k, seed, level] => {
|
||||
let kind = crate::gfx::SPRITE_KINDS.get(*k as usize).copied().unwrap_or(EK::Kobold);
|
||||
let n = full_name(kind, *seed, *level);
|
||||
if n.is_empty() { tr(st(kind).name) } else { n }
|
||||
}
|
||||
_ => a.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
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 < 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();
|
||||
let mut best: Option<(usize, i32)> = None;
|
||||
for dy in -6..=6 {
|
||||
for dx in -6..=6 {
|
||||
let (tx, ty) = (x + dx, y + dy);
|
||||
if !inb(tx, ty) {
|
||||
continue;
|
||||
}
|
||||
let i = idx(tx, ty);
|
||||
let t = &self.map.t[i];
|
||||
if t.kind == Kind::Floor && t.owner == owner && !taken.contains(&i) {
|
||||
let d = dx * dx + dy * dy;
|
||||
if best.map_or(true, |b| d < b.1) {
|
||||
best = Some((i, d));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((tile, _)) = best else { return false };
|
||||
if self.memorials.len() >= MAX_MEMORIALS {
|
||||
self.memorials.remove(0);
|
||||
}
|
||||
self.memorials.push(Memorial { tile, kind: e.kind, name: e.name, level: e.level });
|
||||
if e.side == self.local_side {
|
||||
self.tell_local("msg.creature_fallen", vec![arg(e)], None);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// stones on ground that was lost or built over are taken away
|
||||
pub(crate) fn tidy_memorials(&mut self) {
|
||||
let map = &self.map;
|
||||
self.memorials.retain(|m| map.t[m.tile].kind == Kind::Floor && Owner::MASTER_SIDES.contains(&map.t[m.tile].owner.side()));
|
||||
}
|
||||
|
||||
pub fn memorial_at(&self, i: usize) -> Option<&Memorial> {
|
||||
self.memorials.iter().find(|m| m.tile == i)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn creatures_have_names_and_earn_epithets() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let id = g.spawn(EK::Ogre, false, cx as f32 + 0.5, cy as f32 + 4.5, 1);
|
||||
let e = g.ents.iter().find(|e| e.id == id).unwrap().clone();
|
||||
assert!(e.name != 0);
|
||||
let n1 = full_name(e.kind, e.name, 1);
|
||||
assert!(!n1.is_empty() && !n1.contains("names."), "{}", n1);
|
||||
let n5 = full_name(e.kind, e.name, 5);
|
||||
let n9 = full_name(e.kind, e.name, 9);
|
||||
assert!(n5.starts_with(&n1) || n5.ends_with(&n1), "{} / {}", n1, n5);
|
||||
assert!(n5.len() > n1.len() && n9 != n5, "{} {} {}", n1, n5, n9);
|
||||
assert_eq!(from_arg(arg(&e).trim_start_matches('%')), display_name(&e));
|
||||
// heroes have none
|
||||
let h = g.spawn(EK::Knight, true, cx as f32 + 0.5, cy as f32 + 5.5, 1);
|
||||
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);
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy + 4..cy + 8 {
|
||||
for x in cx..cx + 4 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
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].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);
|
||||
let j = g.ents.iter().position(|e| e.id == k).unwrap();
|
||||
g.kill(j);
|
||||
assert_eq!(g.memorials.len(), 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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-1
@@ -86,7 +86,9 @@ impl Game {
|
||||
let (mx, my) = xy(m);
|
||||
(mx - x).abs() <= 1 && (my - y).abs() <= 1
|
||||
}) {
|
||||
return; // there is one already
|
||||
// a second click (tap) on a marker takes it away again
|
||||
self.unmark(x as f32 + 0.5, y as f32 + 0.5);
|
||||
return;
|
||||
}
|
||||
if marks.len() >= MAX_MARKS {
|
||||
self.hint(31, "hint.marks_full");
|
||||
@@ -107,6 +109,8 @@ impl Game {
|
||||
// a right click with an order tool: the marker there goes (its creatures are free again);
|
||||
// without a marker there the guards stand down
|
||||
pub fn unmark(&mut self, wx: f32, wy: f32) {
|
||||
// the command markers there go with it
|
||||
self.marks.retain(|m| (m.x - wx).powi(2) + (m.y - wy).powi(2) >= 2.0 * 2.0);
|
||||
let near = |m: &usize| {
|
||||
let (mx, my) = xy(*m);
|
||||
(mx as f32 + 0.5 - wx).powi(2) + (my as f32 + 0.5 - wy).powi(2) < 1.6 * 1.6
|
||||
@@ -128,6 +132,11 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(f) = self.flag.filter(|f| near(f)) {
|
||||
// the rally flag goes the same way
|
||||
let (fx, fy) = xy(f);
|
||||
self.set_flag(fx, fy);
|
||||
return;
|
||||
} else {
|
||||
self.cancel_orders();
|
||||
return;
|
||||
@@ -329,6 +338,31 @@ mod tests {
|
||||
assert_eq!(g.ents.iter().filter(|e| matches!(e.job, Job::Guard(_))).count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_tap_removes_a_marker_and_right_click_takes_the_flag() {
|
||||
let mut g = Game::new(14, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy - 6..=cy + 8 {
|
||||
for x in cx - 6..=cx + 10 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
if t.kind != Kind::Core {
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.place_mark(cx + 5, cy + 5, false);
|
||||
g.place_mark(cx + 7, cy + 2, true);
|
||||
assert_eq!((g.guard_marks.len(), g.attack_marks.len()), (1, 1));
|
||||
g.place_mark(cx + 5, cy + 5, false);
|
||||
g.place_mark(cx + 7, cy + 2, true);
|
||||
assert_eq!((g.guard_marks.len(), g.attack_marks.len()), (0, 0), "tapped again: gone");
|
||||
g.set_flag(cx + 4, cy - 3);
|
||||
assert!(g.flag.is_some());
|
||||
g.unmark(cx as f32 + 4.5, cy as f32 - 2.5);
|
||||
assert!(g.flag.is_none(), "the flag goes too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shields_are_held_and_swords_share_the_creatures() {
|
||||
let mut g = Game::new(14, 64, 64);
|
||||
|
||||
+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(""), "_");
|
||||
}
|
||||
}
|
||||
+197
-35
@@ -34,6 +34,9 @@ pub struct Cam {
|
||||
pub render_init: bool,
|
||||
}
|
||||
|
||||
pub const DEFAULT_YAW: f32 = 0.785;
|
||||
pub const DEFAULT_PITCH: f32 = 0.6155;
|
||||
|
||||
fn zero_f32() -> f32 {
|
||||
0.0
|
||||
}
|
||||
@@ -44,7 +47,13 @@ fn cam_uninit() -> bool {
|
||||
|
||||
impl Cam {
|
||||
pub fn new(tx: f32, tz: f32) -> Cam {
|
||||
Cam { tx, tz, yaw: 0.785, pitch: 0.6155, zoom: 18.0, rx: tx, rz: tz, render_init: true }
|
||||
Cam { tx, tz, yaw: DEFAULT_YAW, pitch: DEFAULT_PITCH, zoom: 18.0, rx: tx, rz: tz, render_init: true }
|
||||
}
|
||||
|
||||
// the standard view angle and turn; the position and zoom stay
|
||||
pub fn reset_view(&mut self) {
|
||||
self.yaw = DEFAULT_YAW;
|
||||
self.pitch = DEFAULT_PITCH;
|
||||
}
|
||||
|
||||
pub fn snap(&mut self) {
|
||||
@@ -76,12 +85,18 @@ impl Cam {
|
||||
vec3(self.rx, 0.0, self.rz)
|
||||
}
|
||||
|
||||
// visible world height in tiles: the zoom counts on the shorter side (upright screens: across)
|
||||
pub fn view_h(&self) -> f32 {
|
||||
let (w, h) = (screen_width().max(1.0), screen_height().max(1.0));
|
||||
if h > w { self.zoom * h / w } else { self.zoom }
|
||||
}
|
||||
|
||||
pub fn camera(&self) -> Camera3D {
|
||||
Camera3D {
|
||||
position: self.target() + self.eye_dir() * 60.0,
|
||||
target: self.target(),
|
||||
up: Vec3::Y,
|
||||
fovy: self.zoom,
|
||||
fovy: self.view_h(),
|
||||
projection: Projection::Orthographics,
|
||||
z_near: 1.0,
|
||||
z_far: 200.0,
|
||||
@@ -105,7 +120,7 @@ impl Cam {
|
||||
|
||||
pub fn ray(&self, sx: f32, sy: f32) -> (Vec3, Vec3) {
|
||||
let (f, r, u) = self.basis();
|
||||
let half_h = self.zoom * 0.5;
|
||||
let half_h = self.view_h() * 0.5;
|
||||
let half_w = half_h * screen_width() / screen_height();
|
||||
let nx = sx / screen_width() * 2.0 - 1.0;
|
||||
let ny = 1.0 - sy / screen_height() * 2.0;
|
||||
@@ -497,6 +512,11 @@ fn room_level_at(map: &Map, t: &Tile) -> u8 {
|
||||
}
|
||||
|
||||
// Ambient light field: own territory is lit, foreign areas are dim
|
||||
thread_local! {
|
||||
// daylight above ground (bad weather dims it), in tenths
|
||||
static SKY_LIGHT: std::cell::Cell<u32> = const { std::cell::Cell::new(16) };
|
||||
}
|
||||
|
||||
fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
let mut f = vec![0.0f32; n()];
|
||||
for (i, t) in map.t.iter().enumerate() {
|
||||
@@ -507,7 +527,7 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
};
|
||||
// daylight above ground (cliffs and trees too)
|
||||
if map.sky(i) {
|
||||
f[i] = 1.6;
|
||||
f[i] = SKY_LIGHT.with(|s| s.get()) as f32 / 10.0;
|
||||
}
|
||||
// rooms of a higher level shine brighter
|
||||
let lv = room_level_at(map, t);
|
||||
@@ -517,6 +537,10 @@ fn ambient_field(map: &Map) -> Vec<f32> {
|
||||
if t.river() == Some(Kind::Lava) {
|
||||
f[i] = f[i].max(0.8); // lava lights up its surroundings
|
||||
}
|
||||
// the spores of a mushroom grove glow faintly
|
||||
if t.kind == Kind::Grove || (t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0) {
|
||||
f[i] = f[i].max(0.45);
|
||||
}
|
||||
if t.kind == Kind::Core {
|
||||
f[i] = 1.0;
|
||||
}
|
||||
@@ -675,18 +699,10 @@ fn view_side() -> u8 {
|
||||
VIEW_SIDE.with(|v| v.get())
|
||||
}
|
||||
|
||||
// colour of a dig mark: own marks yellow, other masters' marks in their colour
|
||||
// colour of a dig mark: always the colour of the master who set it
|
||||
fn mark_rgb(by: u8) -> [u8; 3] {
|
||||
if by == view_side() {
|
||||
return [255, 205, 50];
|
||||
}
|
||||
match by {
|
||||
0 => [235, 90, 125], // the first master, seen by another one
|
||||
2 => [80, 240, 110], // green rival
|
||||
3 => [100, 150, 255], // blue rival
|
||||
4 => [255, 150, 50], // the fourth master
|
||||
_ => [255, 205, 50],
|
||||
}
|
||||
let c = master_rgb(by);
|
||||
[(c.x * 255.0) as u8, (c.y * 255.0) as u8, (c.z * 255.0) as u8]
|
||||
}
|
||||
|
||||
// Glowing box in the marker's colour around a block that is marked for digging
|
||||
@@ -764,13 +780,17 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
|
||||
if map.sky(idx(x, z)) && t.kind == Kind::Floor && t.owner == Owner::None {
|
||||
tex = if season() == 2 { T_SNOW } else { T_GRASS } + (t.var as u32 & 3);
|
||||
}
|
||||
// a grove's mushroom stands on moss
|
||||
if t.kind == Kind::Mushroom && t.res & MUSH_GROVE != 0 {
|
||||
tex = crate::gfx::T_MOSS + (t.var as u32 & 3);
|
||||
}
|
||||
let (ua, ub) = cell_uv(tex);
|
||||
let (xf, zf) = (x as f32, z as f32);
|
||||
if !map.shown(idx(x, z)) {
|
||||
// fog of war: a black block, with black sides towards known open ground;
|
||||
// tiles marked for digging glow dark yellow so the plan stays visible
|
||||
// own tiles marked for digging glow darkly in the master's colour so the plan stays visible
|
||||
let black = col(Vec3::splat(0.0), 1.0);
|
||||
let top = if t.marked && t.mark_by == view_side() { col(vec3(0.42, 0.33, 0.06), 1.0) } else { black };
|
||||
let top = if t.marked && t.mark_by == view_side() { col(master_rgb(t.mark_by) * 0.42, 1.0) } else { black };
|
||||
mb.quad([vec3(xf, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf), vec3(xf + 1.0, WALL_H, zf + 1.0), vec3(xf, WALL_H, zf + 1.0)], [ua, ua, ua, ua], [top; 4], [Vec3::Y; 4]);
|
||||
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
|
||||
let open = map.get(x + dx, z + dz).map_or(false, |o| !o.solid()) && map.shown(idx((x + dx).max(0), (z + dz).max(0)));
|
||||
@@ -975,11 +995,16 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
|
||||
let tint = |v: Vec3| col(lava_lit(if t.marked { v * vec3(1.3, 1.15, 0.5) } else { v }, lg), 1.0);
|
||||
let s = 0.85 + 0.1 * (t.var & 3) as f32;
|
||||
if t.kind == Kind::Mushroom {
|
||||
// it grows from a sprout to full size in steps; grown, its cap glows teal
|
||||
let stage = mushroom_stage(&t);
|
||||
let s = s * (0.3 + 0.7 * stage as f32 / 4.0);
|
||||
let h = 0.75 * s;
|
||||
let sc = tint(Vec3::splat(a.max(0.35)));
|
||||
mb.boxy(vec3(cx - 0.12, 0.0, cz - 0.12), vec3(cx + 0.12, h, cz + 0.12), T_STEM, sc, sc);
|
||||
// the cap glows a little in the dark
|
||||
let cc = tint(Vec3::splat(a.max(0.7)));
|
||||
let w = 0.12 * s.max(0.6);
|
||||
mb.boxy(vec3(cx - w, 0.0, cz - w), vec3(cx + w, h, cz + w), T_STEM, sc, sc);
|
||||
// the cap glows a little in the dark; a grown one in its own colour
|
||||
let hue = if stage >= 4 { vec3(0.55, 1.35, 1.25) } else { vec3(0.95, 0.8, 0.62) };
|
||||
let cc = tint(Vec3::splat(a.max(0.7)) * hue);
|
||||
mb.boxy(vec3(cx - 0.42 * s, h, cz - 0.42 * s), vec3(cx + 0.42 * s, h + 0.26, cz + 0.42 * s), T_CAP, cc, cc);
|
||||
mb.boxy(vec3(cx - 0.26 * s, h + 0.26, cz - 0.26 * s), vec3(cx + 0.26 * s, h + 0.38, cz + 0.26 * s), T_CAP, cc, cc);
|
||||
} else {
|
||||
@@ -1201,6 +1226,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
|
||||
Kind::Farm => t.food as u64,
|
||||
Kind::Treasury => (t.gold / 50) as u64 | (t.res as u64) << 5,
|
||||
Kind::Drawbridge => t.res as u64,
|
||||
Kind::Mushroom => mushroom_stage(&t) as u64 | (t.res as u64) << 3,
|
||||
_ => 0,
|
||||
};
|
||||
let shown = map.shown(idx(x, z)) as u64 | (map.sky(idx(x, z)) as u64) << 1;
|
||||
@@ -1258,6 +1284,14 @@ impl R3d {
|
||||
fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
|
||||
self.ensure_chunks();
|
||||
let side = g.me.side().min(7);
|
||||
// the weather dims the daylight: the chunks above ground are lit again in steps
|
||||
let sky = ((1.6 * (1.0 - g.weather.gloom())) * 10.0).round() as u32;
|
||||
if SKY_LIGHT.with(|s| s.get()) != sky {
|
||||
SKY_LIGHT.with(|s| s.set(sky));
|
||||
for ch in self.chunks.iter_mut() {
|
||||
ch.sig = 0;
|
||||
}
|
||||
}
|
||||
if view_side() != side {
|
||||
// another master looks: the marks change colour
|
||||
VIEW_SIDE.with(|v| v.set(side));
|
||||
@@ -1327,8 +1361,9 @@ impl R3d {
|
||||
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
|
||||
push(vec3(cx as f32 + 1.5, 1.6, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
|
||||
for (k, &(px, py)) in g.map.portals.iter().enumerate() {
|
||||
let owned = g.map.portal_owner(k) == g.me;
|
||||
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 20.0);
|
||||
let tint = portal_tint(g, k);
|
||||
let c = tint.map_or(vec3(0.6, 0.55, 0.7), |c| c * 0.8 + vec3(0.1, 0.05, 0.15));
|
||||
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, c, if tint.is_some() { 1.0 } else { 0.35 }, 20.0);
|
||||
}
|
||||
if g.gate_hp > 0.0 {
|
||||
let (gx, gz) = g.gate_center();
|
||||
@@ -1452,7 +1487,7 @@ impl R3d {
|
||||
// Entity under the mouse (screen-space test against the projected sprite)
|
||||
pub fn pick_ent(&self, g: &Game, cam: &Cam, sx: f32, sy: f32, player_only: bool) -> Option<usize> {
|
||||
let m = cam.matrix();
|
||||
let px_per_tile = screen_height() / cam.zoom;
|
||||
let px_per_tile = screen_height() / cam.view_h();
|
||||
let mut best = None;
|
||||
let mut bd = (px_per_tile * 0.4).max(10.0);
|
||||
for (j, e) in g.ents.iter().enumerate() {
|
||||
@@ -1478,7 +1513,9 @@ impl R3d {
|
||||
// on the virtual screen (ui.rs) the world takes exactly k x k interface pixels per pixel
|
||||
let vrows = crate::ui::virtual_rows();
|
||||
let pix = if vrows > 0 && self.pixel_rows > 0 {
|
||||
let k = (vrows / self.pixel_rows).max(1);
|
||||
// the pixel rows count on the shorter side (upright screens: across)
|
||||
let short = vrows.min(crate::ui::sw().round() as u32);
|
||||
let k = (short / self.pixel_rows).max(1);
|
||||
let th = vrows / k;
|
||||
Some(((crate::ui::sw() / k as f32).ceil() as u32, th, k as f32))
|
||||
} else {
|
||||
@@ -1585,6 +1622,7 @@ impl R3d {
|
||||
}
|
||||
draw_traps_glow(g, t);
|
||||
draw_parts_glow(g);
|
||||
draw_weather(g, cam, t);
|
||||
gl_use_default_material();
|
||||
crate::ui::begin_ui();
|
||||
if let (Some((_, _, k)), Some((rt, tw, th))) = (pix, self.target.as_ref()) {
|
||||
@@ -1605,7 +1643,7 @@ impl R3d {
|
||||
let (ix, iz) = (x as i32, z as i32);
|
||||
let own = g.map.get(ix, iz).map_or(false, |t| matches!(t.owner, Owner::Player | Owner::Rival1 | Owner::Rival2 | Owner::Rival3));
|
||||
if inb(ix, iz) && g.map.sky(idx(ix, iz)) {
|
||||
0.95
|
||||
0.95 * (1.0 - 0.6 * g.weather.gloom())
|
||||
} else if own {
|
||||
0.75
|
||||
} else {
|
||||
@@ -2356,6 +2394,39 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
|
||||
// soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains
|
||||
let (cx, cy) = g.map.core;
|
||||
draw_throne(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), t);
|
||||
// memorial stones of fallen creatures: a headstone on a little plinth, a rune that glows
|
||||
for m in &g.memorials {
|
||||
if !g.map.shown(m.tile) {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = xy(m.tile);
|
||||
let h = (m.name % 7) as f32 * 0.02;
|
||||
let c = vec3(x as f32 + 0.5 + ((m.name >> 3) % 5) as f32 * 0.04 - 0.08, 0.0, y as f32 + 0.5);
|
||||
let stone = Color::from_rgba(118, 114, 124, 255);
|
||||
draw_cube(c + vec3(0.0, 0.04, 0.0), vec3(0.42, 0.08, 0.26), None, Color::from_rgba(70, 66, 74, 255));
|
||||
draw_cube(c + vec3(0.0, 0.3 + h * 0.5, 0.0), vec3(0.3, 0.46 + h, 0.1), None, stone);
|
||||
draw_cube(c + vec3(0.0, 0.56 + h, 0.0), vec3(0.2, 0.06, 0.1), None, stone);
|
||||
let glow = if m.level >= 8 { Color::from_rgba(255, 210, 90, 255) } else { Color::from_rgba(150, 120, 255, 255) };
|
||||
draw_cube(c + vec3(0.0, 0.34 + h * 0.5, 0.052), vec3(0.06, 0.14, 0.01), None, glow);
|
||||
}
|
||||
// a ranger's snares: iron jaws on the floor, open and waiting
|
||||
for &i in &g.snares {
|
||||
if !g.map.shown(i) {
|
||||
continue;
|
||||
}
|
||||
let (x, y) = xy(i);
|
||||
let c = vec3(x as f32 + 0.5, 0.0, y as f32 + 0.5);
|
||||
let iron = Color::from_rgba(96, 92, 100, 255);
|
||||
let dark = Color::from_rgba(40, 36, 42, 255);
|
||||
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.34, 0.03, 0.34), None, dark);
|
||||
for side in [-1.0f32, 1.0] {
|
||||
draw_cube(c + vec3(side * 0.16, 0.06, 0.0), vec3(0.04, 0.08, 0.36), None, iron);
|
||||
for k in 0..3 {
|
||||
let z = -0.12 + k as f32 * 0.12;
|
||||
draw_cube(c + vec3(side * 0.12, 0.11, z), vec3(0.03, 0.05, 0.03), None, Color::from_rgba(200, 200, 210, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
// the other masters' cores: the same throne (their hearts glow in their colours)
|
||||
for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) {
|
||||
let (rx, rz) = rv.centre();
|
||||
@@ -2367,9 +2438,10 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
|
||||
if !g.map.shown(idx(px + 1, py + 1)) {
|
||||
continue;
|
||||
}
|
||||
let owned = g.map.portal_owner(pk) == g.me;
|
||||
let tint = portal_tint(g, pk);
|
||||
let owned = tint.is_some();
|
||||
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
|
||||
let stone = |k: u8| if owned { Color::from_rgba(46 + k, 30 + k / 2, 58 + k, 255) } else { Color::from_rgba(56 + k, 54 + k, 60 + k, 255) };
|
||||
let stone = |k: u8| if owned { hue_to(tint, Color::from_rgba(46 + k, 30 + k / 2, 58 + k, 255)) } else { Color::from_rgba(56 + k, 54 + k, 60 + k, 255) };
|
||||
let bone = if owned { Color::from_rgba(212, 200, 176, 255) } else { Color::from_rgba(150, 146, 140, 255) };
|
||||
draw_cube(pc + vec3(0.0, 0.05, 0.0), vec3(2.9, 0.1, 2.9), None, stone(0));
|
||||
draw_cube(pc + vec3(0.0, 0.14, 0.0), vec3(2.4, 0.08, 1.4), None, stone(8));
|
||||
@@ -2446,6 +2518,78 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
|
||||
let _ = t;
|
||||
}
|
||||
|
||||
// rain streaks and fog veils over the land above ground near the camera; a storm's lightning
|
||||
// lights the sky up for a moment
|
||||
fn draw_weather(g: &Game, cam: &Cam, t: f32) {
|
||||
let w = &g.weather;
|
||||
if w.level <= 0.01 || g.map.sky.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (cx, cz) = (cam.rx as i32, cam.rz as i32);
|
||||
let reach = (cam.view_h() * 1.2) as i32 + 4;
|
||||
let sky = |x: i32, z: i32| inb(x, z) && g.map.sky(idx(x, z));
|
||||
// fog: veils at two heights, one plane per run of open sky tiles in a row
|
||||
let fog = w.fog();
|
||||
if fog > 0.01 {
|
||||
for (h, a) in [(0.7f32, 0.22f32), (1.6, 0.16)] {
|
||||
let col = Color::new(0.75, 0.78, 0.8, a * fog);
|
||||
for z in cz - reach..=cz + reach {
|
||||
let mut x = cx - reach;
|
||||
while x <= cx + reach {
|
||||
if !sky(x, z) {
|
||||
x += 1;
|
||||
continue;
|
||||
}
|
||||
let x0 = x;
|
||||
while x <= cx + reach && sky(x, z) {
|
||||
x += 1;
|
||||
}
|
||||
let len = (x - x0) as f32;
|
||||
let drift = (t * 0.2 + z as f32 * 0.7).sin() * 0.15;
|
||||
draw_plane(vec3(x0 as f32 + len * 0.5 + drift, h, z as f32 + 0.5), vec2(len * 0.5, 0.5), None, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// rain: slanted streaks falling over the open land
|
||||
let rain = w.rain();
|
||||
if rain > 0.01 {
|
||||
let n = (1100.0 * rain) as u32;
|
||||
let col = Color::new(0.75, 0.82, 0.95, 0.8);
|
||||
let span = (2 * reach + 1) as u32;
|
||||
for k in 0..n {
|
||||
let h = k.wrapping_mul(2654435761) ^ 0x9e37_79b9;
|
||||
let x = cx - reach + (h % span) as i32;
|
||||
let z = cz - reach + ((h >> 11) % span) as i32;
|
||||
if !sky(x, z) {
|
||||
continue;
|
||||
}
|
||||
let fx = x as f32 + ((h >> 3) % 100) as f32 / 100.0;
|
||||
let fz = z as f32 + ((h >> 17) % 100) as f32 / 100.0;
|
||||
let fall = (t * 1.6 + (h % 1000) as f32 / 1000.0).fract();
|
||||
let y = 3.0 * (1.0 - fall);
|
||||
line3(vec3(fx, y + 0.5, fz - 0.08), vec3(fx + 0.05, y, fz + 0.08), 0.035, col);
|
||||
}
|
||||
}
|
||||
// the flash of a lightning bolt
|
||||
let since = g.time - w.flash;
|
||||
if w.flash > 0.0 && (0.0..0.25).contains(&since) {
|
||||
let a = 0.35 * (1.0 - since / 0.25);
|
||||
for z in (cz - reach..=cz + reach).step_by(4) {
|
||||
draw_plane(vec3(cx as f32, 0.05, z as f32 + 2.0), vec2(reach as f32, 2.0), None, Color::new(0.9, 0.92, 1.0, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// growth stage of a giant mushroom: 0 a sprout .. 4 fully grown (felling only then)
|
||||
fn mushroom_stage(t: &Tile) -> u32 {
|
||||
if t.grown() {
|
||||
4
|
||||
} else {
|
||||
((t.growth() * 4.0) as u32).min(3)
|
||||
}
|
||||
}
|
||||
|
||||
// the colour of a master side (the first master red, rivals green, blue and yellow)
|
||||
pub fn master_rgb(side: u8) -> Vec3 {
|
||||
match side {
|
||||
@@ -2456,6 +2600,12 @@ pub fn master_rgb(side: u8) -> Vec3 {
|
||||
}
|
||||
}
|
||||
|
||||
// a portal claimed by a master glows in his colour; None: unclaimed (grey and still)
|
||||
fn portal_tint(g: &Game, k: usize) -> Option<Vec3> {
|
||||
let side = g.map.portal_owner(k).side();
|
||||
Owner::MASTER_SIDES.contains(&side).then(|| master_rgb(side))
|
||||
}
|
||||
|
||||
// recolours a red-tinted colour into `tint`: keeps its brightness and whiteness, swaps the hue
|
||||
fn hue_to(tint: Option<Vec3>, c: Color) -> Color {
|
||||
let Some(t) = tint else { return c };
|
||||
@@ -2543,13 +2693,15 @@ fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) {
|
||||
if !g.map.shown(idx(px + 1, py + 1)) {
|
||||
continue;
|
||||
}
|
||||
let owned = g.map.portal_owner(pk) == g.me;
|
||||
let tint = portal_tint(g, pk);
|
||||
let owned = tint.is_some();
|
||||
let tc = |c: Color| hue_to(tint, c);
|
||||
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
|
||||
let arm = if owned { Color::new(0.8, 0.35, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
||||
let arm = if owned { tc(Color::new(0.8, 0.35, 1.0, 1.0)) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
|
||||
let mid = pc + vec3(0.0, 1.3, 0.0);
|
||||
let (rx, ry) = (0.95, 1.55);
|
||||
// the dark heart of the vortex: a vertical disc in the arch
|
||||
vdisc(mid, rx, ry, if owned { Color::new(0.16, 0.02, 0.26, 0.92) } else { Color::new(0.08, 0.08, 0.1, 0.85) }, 28);
|
||||
vdisc(mid, rx, ry, if owned { tc(Color::new(0.16, 0.02, 0.26, 0.92)) } else { Color::new(0.08, 0.08, 0.1, 0.85) }, 28);
|
||||
let speed = if owned { 2.0 } else { 0.25 };
|
||||
for armk in 0..3 {
|
||||
for k in 0..22 {
|
||||
@@ -2572,16 +2724,16 @@ fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) {
|
||||
draw_cube(p, vec3(0.12, 0.14, 0.02), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.35 + 0.65 * glow } else { 0.2 }));
|
||||
}
|
||||
if owned {
|
||||
draw_sphere(mid, 0.35 + 0.05 * (t * 3.0).sin(), None, Color::new(0.95, 0.75, 1.0, 0.5));
|
||||
draw_sphere(mid, 0.35 + 0.05 * (t * 3.0).sin(), None, tc(Color::new(0.95, 0.75, 1.0, 0.5)));
|
||||
// sparks pour out of the vortex and fall to the floor
|
||||
for k in 0..14 {
|
||||
let e = (t * 0.5 + k as f32 * 0.071).fract();
|
||||
let a = k as f32 * 2.4;
|
||||
let dir = if k % 2 == 0 { 1.0 } else { -1.0 };
|
||||
let p = mid + vec3(a.cos() * 0.5 * (1.0 - e), a.sin() * 0.6 + e * 0.4 - e * e * 1.9, dir * e * 1.1);
|
||||
draw_cube(p, Vec3::splat(0.045 * (1.0 - e) + 0.015), None, Color::new(0.95, 0.65, 1.0, 1.0 - e));
|
||||
draw_cube(p, Vec3::splat(0.045 * (1.0 - e) + 0.015), None, tc(Color::new(0.95, 0.65, 1.0, 1.0 - e)));
|
||||
}
|
||||
disc(pc + vec3(0.0, 0.2, 0.0), 1.1, Color::new(0.6, 0.25, 0.9, 0.12 + 0.05 * (t * 2.0).sin()), 24);
|
||||
disc(pc + vec3(0.0, 0.2, 0.0), 1.1, tc(Color::new(0.6, 0.25, 0.9, 0.12 + 0.05 * (t * 2.0).sin())), 24);
|
||||
}
|
||||
}
|
||||
// gate light
|
||||
@@ -2756,7 +2908,7 @@ fn draw_marks(g: &Game, t: f32) {
|
||||
return;
|
||||
}
|
||||
for m in &g.marks {
|
||||
if !m.fixed && !g.debug_markers {
|
||||
if !g.debug_markers {
|
||||
continue;
|
||||
}
|
||||
let left = m.until.saturating_sub(g.sim_steps) as f32 * STEP;
|
||||
@@ -3225,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);
|
||||
}
|
||||
@@ -3326,6 +3487,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
|
||||
Kind::Drawbridge => [120, 80, 50, 255],
|
||||
Kind::Obsidian => [60, 40, 70, 255],
|
||||
Kind::Mushroom => [150, 90, 200, 255],
|
||||
Kind::Grove => [50, 110, 92, 255],
|
||||
Kind::Tree => [40, 100, 40, 255],
|
||||
Kind::Shard => [255, 60, 120, 255],
|
||||
Kind::Aerie => [170, 140, 90, 255],
|
||||
|
||||
+114
-20
@@ -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 {
|
||||
@@ -97,26 +101,8 @@ impl Game {
|
||||
pub fn add_rival(&mut self, side: u8, cx: i32, cy: i32, difficulty: u8) {
|
||||
let core = (cx - 1, cy - 1);
|
||||
self.carve_master_area(side, core);
|
||||
// his own portal beside the dungeon (if there is room)
|
||||
let owner = Owner::of_side(side);
|
||||
let (px, py) = (core.0 + 6, core.1);
|
||||
if px + 3 < mw() as i32 - 2 && py >= 2 && py + 3 < mh() as i32 - 2 {
|
||||
for y in py..py + 3 {
|
||||
for x in px..px + 3 {
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
if !matches!(t.kind, Kind::Core | Kind::Gate) {
|
||||
t.kind = Kind::Portal;
|
||||
t.owner = owner;
|
||||
t.gold = 0;
|
||||
t.reinforced = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.map.portals.contains(&(px, py)) {
|
||||
self.map.portals.push((px, py));
|
||||
}
|
||||
}
|
||||
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.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, focus: None, defending: false });
|
||||
self.adopt_master(side, core, difficulty, true);
|
||||
}
|
||||
|
||||
@@ -468,6 +454,60 @@ mod tests {
|
||||
assert!(ground > ground0 + 20, "the yellow master digs and claims: {} -> {}", ground0, ground);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_marks_stay_near_its_ground_and_imps_dig_only_their_masters_marks() {
|
||||
let mut g = rival_game(2);
|
||||
let side = g.rivals[0].side;
|
||||
let owner = Owner::of_side(side);
|
||||
for _ in 0..(10 * 60 * 20) {
|
||||
g.update(0.05);
|
||||
if g.time as i32 % 20 == 0 {
|
||||
for (i, t) in g.map.t.iter().enumerate() {
|
||||
if t.marked && t.mark_by == side {
|
||||
let (x, y) = crate::map::xy(i);
|
||||
let near = (-5..=5).any(|dy| (-5..=5).any(|dx| g.map.get(x + dx, y + dy).map_or(false, |o| o.owner == owner)));
|
||||
assert!(near, "AI mark at {},{} far from its ground", x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// a foreign mark next to the player's imps stays untouched
|
||||
let (cx, cy) = g.map.core;
|
||||
let spot = (0..g.map.t.len()).find(|&i| {
|
||||
let (x, y) = crate::map::xy(i);
|
||||
(x - cx).abs() < 10 && (y - cy).abs() < 10 && g.map.t[i].kind == Kind::Earth && !g.map.t[i].marked
|
||||
&& DIR4.iter().any(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |o| o.owner == Owner::Player && o.walkable()))
|
||||
}).expect("earth beside the player's floor");
|
||||
g.map.t[spot].reinforced = false;
|
||||
g.map.t[spot].marked = true;
|
||||
g.map.t[spot].mark_by = side;
|
||||
for _ in 0..(60 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert_eq!(g.map.t[spot].kind, Kind::Earth, "the player's imps leave the AI's mark alone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ai_summons_new_imps_when_its_imps_die() {
|
||||
let mut g = rival_game(1);
|
||||
let side = g.rivals[0].side;
|
||||
for _ in 0..(60 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
for e in g.ents.iter_mut().filter(|e| e.alive && e.side == side && e.kind == EK::Worker) {
|
||||
e.hp = 0.0;
|
||||
}
|
||||
for _ in 0..20 {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert_eq!(g.ents.iter().filter(|e| e.alive && e.side == side && e.kind == EK::Worker).count(), 0, "all its imps are dead");
|
||||
for _ in 0..(3 * 60 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
let imps = g.ents.iter().filter(|e| e.alive && e.side == side && e.kind == EK::Worker).count();
|
||||
assert!(imps >= 2, "new imps (one per 80 mana, like the player): {}", imps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_dig_marks_carry_the_masters_side() {
|
||||
let mut g = rival_game(1);
|
||||
@@ -564,6 +604,60 @@ mod tests {
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_ai_partner_builds_only_what_is_researched() {
|
||||
// the first missions of both co-op campaigns: no library, no forge yet
|
||||
let doc = crate::editor::coop_campaign::coop_campaign();
|
||||
let games = [
|
||||
crate::campaign::start_editor_coop_mission(&doc.missions[0], 0, 77),
|
||||
crate::campaign::start_coop_mission(crate::campaign::coop_mission_indexes()[0], None),
|
||||
];
|
||||
let lib = crate::ui::TOOLS.iter().position(|&t| t == crate::ui::Tool::Room(4)).unwrap();
|
||||
let forge = crate::ui::TOOLS.iter().position(|&t| t == crate::ui::Tool::Room(8)).unwrap();
|
||||
for mut g in games {
|
||||
g.coop = true;
|
||||
assert!(g.start_match(&[0, 2], 0));
|
||||
g.sync_ai_sides(&[2], 1);
|
||||
g.end_match();
|
||||
let mine_knows = (g.tool_allowed(lib), g.tool_allowed(forge));
|
||||
for _ in 0..(10 * 60 * 20) {
|
||||
g.update(0.05);
|
||||
}
|
||||
let owner = Owner::of_side(2);
|
||||
let has = |k: Kind| g.map.t.iter().any(|t| t.kind == k && t.owner == owner);
|
||||
if !mine_knows.0 {
|
||||
assert!(!has(Kind::Library), "the partner has no library either");
|
||||
}
|
||||
if !mine_knows.1 {
|
||||
assert!(!has(Kind::Forge), "the partner has no forge either");
|
||||
}
|
||||
assert!(has(Kind::Lair) || has(Kind::Treasury), "but builds what it knows");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imps_take_ground_from_enemies_but_never_from_allies() {
|
||||
let mut g = offline_league_game();
|
||||
assert!(g.allied(0, 2));
|
||||
// a partner floor tile right beside the player's floor
|
||||
let spot = (0..g.map.t.len()).find(|&i| {
|
||||
let (x, y) = crate::map::xy(i);
|
||||
g.map.t[i].kind == Kind::Floor && g.map.t[i].owner == Owner::Player
|
||||
&& DIR4.iter().any(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |o| o.kind == Kind::Floor && o.owner == Owner::Player))
|
||||
&& x > 2 && y > 2
|
||||
}).expect("player floor");
|
||||
let (x, y) = crate::map::xy(spot);
|
||||
let n = DIR4.iter().map(|&(dx, dy)| crate::map::idx(x + dx, y + dy)).find(|&j| g.map.t[j].owner == Owner::Player).unwrap();
|
||||
g.map.t[n].owner = Owner::of_side(2);
|
||||
g.map.t[n].reserved = 0;
|
||||
assert!(!g.claimable(n, 999), "an ally's floor stays his");
|
||||
// the same tile of an enemy master may be taken
|
||||
g.coop = false;
|
||||
g.teams = vec![(0, 0), (2, 1)];
|
||||
assert!(!g.allied(0, 2));
|
||||
assert!(g.claimable(n, 999), "an enemy's floor can be claimed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_offline_coop_partner_is_an_allied_ai_master() {
|
||||
let mut g = offline_league_game();
|
||||
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
// The rogues of the later waves: the thief sneaks into a treasury and runs off with gold, the
|
||||
// monk shrugs off spells, the ranger lays snares in the dungeon's corridors and the inquisitor
|
||||
// now and then turns one of the weakest creatures (kobolds, spiders) to the heroes' side.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
|
||||
// (kind, first wave, first campaign mission (index)) - the wave pool takes them from then on
|
||||
pub const ROGUES: [(EK, u32, usize); 4] = [(EK::Thief, 3, 4), (EK::Monk, 5, 8), (EK::Ranger, 7, 11), (EK::Inquisitor, 9, 15)];
|
||||
|
||||
// gold a thief can carry, and how fast he scoops it
|
||||
pub const THIEF_BAG: i32 = 250;
|
||||
const THIEF_GRAB: i32 = 25;
|
||||
// a ranger's snares: one every SNARE_EVERY seconds, at most MAX_SNARES on the map
|
||||
const SNARE_EVERY: f32 = 18.0;
|
||||
const MAX_SNARES: usize = 30;
|
||||
pub const SNARE_DAMAGE: f32 = 25.0;
|
||||
pub const SNARE_STUN: f32 = 3.0;
|
||||
// the inquisitor turns a weak creature every CONVERT_EVERY seconds, within CONVERT_RANGE tiles
|
||||
pub const CONVERT_EVERY: f32 = 20.0;
|
||||
const CONVERT_RANGE: f32 = 5.0;
|
||||
|
||||
// spells do not touch a monk of the order
|
||||
pub fn spell_proof(e: &Ent) -> bool {
|
||||
e.hero && e.kind == EK::Monk
|
||||
}
|
||||
|
||||
// the creatures an inquisitor can turn: only the weakest
|
||||
pub fn convertible(k: EK) -> bool {
|
||||
matches!(k, EK::Kobold | EK::Spider)
|
||||
}
|
||||
|
||||
fn master_ground(t: &Tile) -> bool {
|
||||
Owner::MASTER_SIDES.contains(&t.owner.side())
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// true: the rogue did all he wanted this step (the usual hero AI is skipped)
|
||||
pub(crate) fn rogue_ai(&mut self, e: &mut Ent, dt: f32) -> bool {
|
||||
if !e.active_hero() {
|
||||
return false;
|
||||
}
|
||||
match e.kind {
|
||||
EK::Thief => self.thief_ai(e, dt),
|
||||
EK::Ranger => {
|
||||
self.lay_snare(e);
|
||||
false
|
||||
}
|
||||
EK::Inquisitor => {
|
||||
self.convert_creature(e);
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ thief
|
||||
|
||||
fn thief_ai(&mut self, e: &mut Ent, dt: f32) -> bool {
|
||||
let cur = e.tile();
|
||||
let full = e.carry >= THIEF_BAG;
|
||||
// a full bag (or nothing left to steal): back to the gate and away
|
||||
if full || (e.carry > 0 && !self.gold_to_steal(cur)) {
|
||||
if !self.has_gate() {
|
||||
return false;
|
||||
}
|
||||
let (gx, gy) = self.gate_center();
|
||||
if (e.x - gx).powi(2) + (e.y - gy).powi(2) < 2.0 * 2.0 {
|
||||
e.alive = false;
|
||||
self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070);
|
||||
self.tell_local("msg.thief_escaped", vec![e.carry.to_string()], Some(Sfx::Error));
|
||||
return true;
|
||||
}
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 1.5;
|
||||
if let Some(p) = self.map.path_to(cur, idx(gx as i32, gy as i32)) {
|
||||
e.path = p;
|
||||
}
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
return true;
|
||||
}
|
||||
// in a treasury with gold: scoop it up
|
||||
let t = self.map.t[cur];
|
||||
if t.kind == Kind::Treasury && t.gold > 0 && master_ground(&t) {
|
||||
e.path.clear();
|
||||
e.working = true;
|
||||
if e.cd <= 0.0 {
|
||||
e.cd = 0.5;
|
||||
let want = THIEF_GRAB.min(THIEF_BAG - e.carry).min(t.gold);
|
||||
let got = self.steal_gold(t.owner.side(), cur, want);
|
||||
if got > 0 && e.carry == 0 && t.owner.side() == self.local_side {
|
||||
self.tell_local("msg.thief_in_treasury", vec![], Some(Sfx::WaveAlarm));
|
||||
}
|
||||
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;
|
||||
}
|
||||
// on the way to the nearest treasury with gold
|
||||
if e.path.is_empty() && e.repath <= 0.0 {
|
||||
e.repath = 2.0;
|
||||
let cost = self.map.hero_cost();
|
||||
let found = self.map.search(cur, cost, |i| {
|
||||
let t = &self.map.t[i];
|
||||
t.kind == Kind::Treasury && t.gold > 0 && master_ground(t)
|
||||
}, true);
|
||||
match found {
|
||||
Some(p) => e.path = p,
|
||||
None if e.carry > 0 => {}
|
||||
None => return false, // nothing to steal: he fights like the others
|
||||
}
|
||||
}
|
||||
if e.path.is_empty() {
|
||||
return e.carry > 0;
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
true
|
||||
}
|
||||
|
||||
// is there a treasury with gold anywhere a thief could reach from tile i?
|
||||
fn gold_to_steal(&self, i: usize) -> bool {
|
||||
let cost = self.map.hero_cost();
|
||||
self.map.search(i, cost, |j| {
|
||||
let t = &self.map.t[j];
|
||||
t.kind == Kind::Treasury && t.gold > 0 && master_ground(t)
|
||||
}, true).is_some()
|
||||
}
|
||||
|
||||
// takes gold from the treasury tile i of the master on `side`; returns what was taken
|
||||
fn steal_gold(&mut self, side: u8, i: usize, want: i32) -> i32 {
|
||||
let me = self.me_side;
|
||||
if side != me {
|
||||
if !self.is_master(side) {
|
||||
return 0;
|
||||
}
|
||||
self.switch_to(side);
|
||||
}
|
||||
let take = (want as f32).min(self.gold).floor().max(0.0) as i32;
|
||||
self.gold -= take as f32;
|
||||
let t = &mut self.map.t[i];
|
||||
t.gold = (t.gold - take).max(0);
|
||||
if side != me {
|
||||
self.switch_to(me);
|
||||
}
|
||||
take
|
||||
}
|
||||
|
||||
// a slain thief drops his loot: it goes to whoever slew him (with the bounty)
|
||||
pub(crate) fn thief_loot(&self, id: u32) -> f32 {
|
||||
self.ents.iter().find(|e| e.id == id).map_or(0.0, |e| e.carry.max(0) as f32)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ ranger
|
||||
|
||||
// a snare on the dungeon's ground under the ranger's feet, now and then
|
||||
fn lay_snare(&mut self, e: &mut Ent) {
|
||||
if e.skill_cd > 0.0 || self.snares.len() >= MAX_SNARES {
|
||||
return;
|
||||
}
|
||||
let i = e.tile();
|
||||
let t = &self.map.t[i];
|
||||
if !t.walkable() || !master_ground(t) || t.bridge() || self.snares.contains(&i) || self.traps.iter().any(|tr| tr.tile == i) {
|
||||
e.skill_cd = 2.0;
|
||||
return;
|
||||
}
|
||||
self.snares.push(i);
|
||||
e.skill_cd = SNARE_EVERY;
|
||||
self.fx(6, e.x, e.y, 0.0, 0.0, 0.4, 0xff3a5a7a);
|
||||
}
|
||||
|
||||
// the shared step: a creature that steps into a snare is caught (hurt and stuck for a while)
|
||||
pub(crate) fn update_snares(&mut self) {
|
||||
if self.snares.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut k = 0;
|
||||
while k < self.snares.len() {
|
||||
let i = self.snares[k];
|
||||
if !self.map.t[i].walkable() {
|
||||
self.snares.swap_remove(k);
|
||||
continue;
|
||||
}
|
||||
let caught = self.ents.iter().position(|e| {
|
||||
e.alive && !e.held && !e.hero && !e.chained && e.captive == 0 && !flies(e.kind) && Owner::MASTER_SIDES.contains(&e.side) && e.tile() == i
|
||||
});
|
||||
let Some(j) = caught else {
|
||||
k += 1;
|
||||
continue;
|
||||
};
|
||||
self.snares.swap_remove(k);
|
||||
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::Snare, x, y);
|
||||
if side == self.local_side {
|
||||
self.tell_local("msg.snare_caught", vec![format!("@{}", st(kind).name)], None);
|
||||
}
|
||||
if dead {
|
||||
self.kill(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ inquisitor
|
||||
|
||||
// turns the nearest weak creature in sight to the heroes' side
|
||||
fn convert_creature(&mut self, e: &mut Ent) {
|
||||
if e.skill_cd > 0.0 {
|
||||
return;
|
||||
}
|
||||
e.skill_cd = 1.0;
|
||||
let target = self
|
||||
.ents
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, o)| o.alive && !o.held && !o.hero && !o.chained && o.captive == 0 && convertible(o.kind) && Owner::MASTER_SIDES.contains(&o.side))
|
||||
.map(|(j, o)| (j, (o.x - e.x).powi(2) + (o.y - e.y).powi(2)))
|
||||
.filter(|&(j, d2)| d2 < CONVERT_RANGE * CONVERT_RANGE && self.los(e.x, e.y, self.ents[j].x, self.ents[j].y))
|
||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||||
.map(|(j, _)| j);
|
||||
let Some(j) = target else { return };
|
||||
e.skill_cd = CONVERT_EVERY;
|
||||
let goal = e.target;
|
||||
let (side, kind, id, x, y) = {
|
||||
let o = &self.ents[j];
|
||||
(o.side, o.kind, o.id, o.x, o.y)
|
||||
};
|
||||
self.release_all(id);
|
||||
let o = &mut self.ents[j];
|
||||
o.hero = true;
|
||||
o.side = 1;
|
||||
o.job = Job::None;
|
||||
o.path.clear();
|
||||
o.home = None;
|
||||
o.bed = None;
|
||||
o.guard = false;
|
||||
o.target = goal;
|
||||
o.waypoint = None;
|
||||
o.flash = 0.4;
|
||||
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::Sermon, x, y);
|
||||
if side == self.local_side {
|
||||
self.tell_local("msg.converted", vec![format!("@{}", st(kind).name)], Some(Sfx::WaveAlarm));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// a free game with an open dungeon around the core and no waves
|
||||
fn open_game() -> Game {
|
||||
let mut g = Game::new(5, 96, 96);
|
||||
g.gate_on = false;
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy - 8..cy + 10 {
|
||||
for x in cx - 8..cx + 10 {
|
||||
let t = &mut g.map.t[idx(x, y)];
|
||||
if t.kind != Kind::Core {
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::Player;
|
||||
}
|
||||
}
|
||||
}
|
||||
g.ents.clear();
|
||||
g.recount();
|
||||
g
|
||||
}
|
||||
|
||||
fn hero(g: &mut Game, k: EK, x: f32, y: f32) -> u32 {
|
||||
let id = g.spawn(k, true, x, y, 1);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.job = Job::None;
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thief_steals_from_the_treasury() {
|
||||
let mut g = open_game();
|
||||
let (cx, cy) = g.map.core;
|
||||
g.build_rect(0, cx + 4, cy + 4, cx + 6, cy + 6);
|
||||
g.gold = 3000.0;
|
||||
g.sync_treasury();
|
||||
let gold0 = g.gold;
|
||||
let id = hero(&mut g, EK::Thief, cx as f32 - 5.5, cy as f32 + 5.5);
|
||||
let mut carried = 0;
|
||||
for _ in 0..(60 * 20) {
|
||||
g.state = 0; // no creatures in the test: it would count as over
|
||||
g.update(0.05);
|
||||
carried = g.ents.iter().find(|e| e.id == id).map_or(carried, |e| e.carry);
|
||||
if carried >= THIEF_BAG {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(carried > 0, "the thief took gold");
|
||||
assert!(g.gold < gold0, "the master lost it: {} -> {}", gold0, g.gold);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monks_shrug_off_spells() {
|
||||
let mut g = open_game();
|
||||
let (cx, cy) = g.map.core;
|
||||
let monk = hero(&mut g, EK::Monk, cx as f32 + 6.5, cy as f32 + 6.5);
|
||||
let knight = hero(&mut g, EK::Knight, cx as f32 + 6.5, cy as f32 + 7.0);
|
||||
g.mana = 400.0;
|
||||
g.lightning(cx as f32 + 6.5, cy as f32 + 6.7);
|
||||
let hp = |g: &Game, id: u32| g.ents.iter().find(|e| e.id == id).map_or(0.0, |e| e.hp);
|
||||
assert_eq!(hp(&g, monk), st(EK::Monk).hp, "the monk is untouched");
|
||||
assert!(hp(&g, knight) < st(EK::Knight).hp, "the knight is not");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rangers_snare_catches_a_creature() {
|
||||
let mut g = open_game();
|
||||
let (cx, cy) = g.map.core;
|
||||
let r = hero(&mut g, EK::Ranger, cx as f32 + 6.5, cy as f32 + 6.5);
|
||||
let mut e = g.ents.iter().find(|e| e.id == r).unwrap().clone();
|
||||
e.skill_cd = 0.0;
|
||||
g.lay_snare(&mut e);
|
||||
let i = idx(cx + 6, cy + 6);
|
||||
assert!(g.snares.contains(&i));
|
||||
g.ents.retain(|e| e.id != r);
|
||||
let k = g.spawn(EK::Ogre, false, cx as f32 + 6.5, cy as f32 + 6.5, 1);
|
||||
g.update_snares();
|
||||
let o = g.ents.iter().find(|e| e.id == k).unwrap();
|
||||
assert!(o.stun > 0.0 && o.hp < st(EK::Ogre).hp, "caught");
|
||||
assert!(g.snares.is_empty(), "a snare works once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_inquisitor_turns_only_weak_creatures() {
|
||||
let mut g = open_game();
|
||||
let (cx, cy) = g.map.core;
|
||||
let ogre = g.spawn(EK::Ogre, false, cx as f32 + 5.5, cy as f32 + 6.0, 1);
|
||||
let kobold = g.spawn(EK::Kobold, false, cx as f32 + 6.5, cy as f32 + 7.5, 1);
|
||||
let q = hero(&mut g, EK::Inquisitor, cx as f32 + 6.5, cy as f32 + 5.5);
|
||||
let mut e = g.ents.iter().find(|e| e.id == q).unwrap().clone();
|
||||
e.skill_cd = 0.0;
|
||||
g.convert_creature(&mut e);
|
||||
let get = |g: &Game, id: u32| g.ents.iter().find(|e| e.id == id).unwrap().clone();
|
||||
assert!(get(&g, kobold).hero, "the kobold now fights for the heroes");
|
||||
assert!(!get(&g, ogre).hero, "an ogre is too strong to be turned");
|
||||
assert_eq!(e.skill_cd, CONVERT_EVERY);
|
||||
// nothing else to turn: the ogre stays
|
||||
e.skill_cd = 0.0;
|
||||
g.convert_creature(&mut e);
|
||||
assert!(!get(&g, ogre).hero);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rogues_join_the_later_waves() {
|
||||
for (k, wave, mission) in ROGUES {
|
||||
assert!(wave >= 3 && mission >= 4, "{:?} comes late", k);
|
||||
}
|
||||
assert!(ROGUES.iter().any(|r| r.0 == EK::Inquisitor && r.1 == 9));
|
||||
}
|
||||
}
|
||||
@@ -224,3 +224,80 @@ fn export_scenes() {
|
||||
run(&mut g, 1.0);
|
||||
save("room-levels", &mut g, cx as f32 + 1.5, cy as f32 - 2.0, 16.0, 0.0, false);
|
||||
}
|
||||
|
||||
// a mushroom grove: moss, grown (glowing) and young mushrooms
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn export_grove_scene() {
|
||||
std::fs::create_dir_all("/tmp/scenes").unwrap();
|
||||
let mut g = Game::new(9, 96, 96);
|
||||
// the grove tile with the most grove around it
|
||||
let best = (0..g.map.t.len())
|
||||
.filter(|&i| g.map.t[i].kind == Kind::Grove)
|
||||
.max_by_key(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
(-3..=3).flat_map(|dy| (-3..=3).map(move |dx| (dx, dy))).filter(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |t| matches!(t.kind, Kind::Grove | Kind::Mushroom))).count()
|
||||
})
|
||||
.unwrap();
|
||||
let (x, y) = xy(best);
|
||||
run(&mut g, 1.0);
|
||||
save("mushroom-grove", &mut g, x as f32 + 0.5, y as f32 + 0.5, 12.0, 0.0, false);
|
||||
}
|
||||
|
||||
// the rogues of the later waves in a dungeon: thief, monk, ranger, inquisitor and a snare
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn export_rogues_scene() {
|
||||
std::fs::create_dir_all("/tmp/scenes").unwrap();
|
||||
let mut g = Game::new(11, 80, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
open(&mut g, cx - 9, cy - 8, cx + 11, cy + 9);
|
||||
g.place_all_torches();
|
||||
for (k, kind) in [EK::Thief, EK::Monk, EK::Ranger, EK::Inquisitor].into_iter().enumerate() {
|
||||
let id = g.spawn(kind, true, cx as f32 + 4.5 + k as f32 * 1.2, cy as f32 + 5.5, 3);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.guard = true;
|
||||
e.home = Some((e.x, e.y));
|
||||
}
|
||||
}
|
||||
g.snares.push(idx(cx + 5, cy + 3));
|
||||
g.snares.push(idx(cx + 7, cy + 3));
|
||||
save("rogues", &mut g, cx as f32 + 6.5, cy as f32 + 4.5, 8.0, 0.0, false);
|
||||
}
|
||||
|
||||
// weather above ground: rain and fog over the land
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn export_weather_scenes() {
|
||||
std::fs::create_dir_all("/tmp/scenes").unwrap();
|
||||
for (name, kind) in [("weather-rain", crate::weather::RAIN), ("weather-fog", crate::weather::FOG), ("weather-storm", crate::weather::STORM)] {
|
||||
let mut g = Game::new(7, 96, 96);
|
||||
g.add_surface(7, 0.5, 3);
|
||||
g.weather = crate::weather::Weather { kind, shown: kind, level: 1.0, left: 900.0, ..Default::default() };
|
||||
// the camera on the land above ground, near the edge of the dungeon side
|
||||
let i = (0..g.map.t.len()).filter(|&i| g.map.sky(i)).min_by_key(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
(x - 52).abs() + (y - 48).abs()
|
||||
}).unwrap();
|
||||
let (x, y) = xy(i);
|
||||
save(name, &mut g, x as f32 + 4.5, y as f32 + 0.5, 18.0, 0.0, false);
|
||||
}
|
||||
}
|
||||
|
||||
// named creatures and memorial stones of the fallen
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn export_names_scene() {
|
||||
std::fs::create_dir_all("/tmp/scenes").unwrap();
|
||||
let mut g = Game::new(11, 80, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
open(&mut g, cx - 9, cy - 8, cx + 11, cy + 9);
|
||||
g.place_all_torches();
|
||||
creatures(&mut g, &[(EK::Ogre, 9), (EK::Mage, 5), (EK::Kobold, 2)], cx as f32 + 6.5, cy as f32 + 5.0);
|
||||
for (k, &(kind, lv)) in [(EK::Spider, 4), (EK::Ogre, 8), (EK::Mage, 6)].iter().enumerate() {
|
||||
let id = g.spawn(kind, false, cx as f32 + 2.5 + k as f32 * 2.0, cy as f32 + 6.5, lv);
|
||||
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||
g.kill(j);
|
||||
}
|
||||
save("names", &mut g, cx as f32 + 5.0, cy as f32 + 5.5, 9.0, 0.0, false);
|
||||
}
|
||||
+3
-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
|
||||
@@ -163,6 +163,7 @@ pub struct Settings {
|
||||
pub hints: bool,
|
||||
pub difficulty: u8, // rival masters in free games: 0 easy, 1 medium, 2 hard
|
||||
pub map_w: usize, // size of new maps (64..256)
|
||||
pub match_size: usize, // versus matches: the map is match_size x match_size (see lobby::seats_for)
|
||||
pub map_h: usize,
|
||||
pub surface: bool, // new free games: the eastern half lies above ground (forest)
|
||||
pub events: bool, // random events in free games and versus matches
|
||||
@@ -201,6 +202,7 @@ impl Default for Settings {
|
||||
hints: true,
|
||||
difficulty: 1,
|
||||
map_w: 64,
|
||||
match_size: 128,
|
||||
map_h: 64,
|
||||
surface: false,
|
||||
events: true,
|
||||
|
||||
+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)
|
||||
|
||||
+2
-2
@@ -74,7 +74,7 @@ impl Spell {
|
||||
}
|
||||
|
||||
fn foe(e: &Ent) -> bool {
|
||||
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0
|
||||
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0 && !crate::rogues::spell_proof(e)
|
||||
}
|
||||
|
||||
fn own(e: &Ent) -> bool {
|
||||
@@ -92,7 +92,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn cast(&mut self, spell: Spell, wx: f32, wy: f32) {
|
||||
let cost = spell.cost();
|
||||
let cost = spell.cost() * self.talents.spell_cost();
|
||||
if self.mana < cost {
|
||||
self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]);
|
||||
self.sfx_ui(Sfx::Error);
|
||||
|
||||
@@ -48,6 +48,8 @@ impl GameStats {
|
||||
impl Game {
|
||||
// counts for the master at this computer (another master's deeds count on his computer)
|
||||
pub(crate) fn count(&mut self, k: usize, n: f32) {
|
||||
// every master's deeds bring him experience for his talents
|
||||
self.add_master_xp(crate::talents::xp_for(k) * n);
|
||||
if self.me_side != self.local_side || self.demo.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
// Master talents: a master earns experience (slain heroes, dug tiles, rooms, walls, traps,
|
||||
// towers, spells, gold) and with it talent points, spent on four branches of five levels each:
|
||||
// Builder (imps dig faster, rooms cost less), Warlord (creatures hit harder and take less
|
||||
// damage), Warlock (more mana, cheaper spells), Merchant (lower wages, more gold from treasure).
|
||||
// Per game in free play and matches; in the campaign they go along from mission to mission.
|
||||
|
||||
use crate::game::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const BRANCHES: usize = 4;
|
||||
pub const BUILDER: usize = 0;
|
||||
pub const WARLORD: usize = 1;
|
||||
pub const WARLOCK: usize = 2;
|
||||
pub const MERCHANT: usize = 3;
|
||||
pub const MAX_LEVEL: u8 = 5;
|
||||
pub const NAMES: [&str; BRANCHES] = ["talent.builder", "talent.warlord", "talent.warlock", "talent.merchant"];
|
||||
|
||||
// experience for the deeds of a master (stats counter -> experience per unit)
|
||||
pub fn xp_for(counter: usize) -> f32 {
|
||||
use crate::stats as k;
|
||||
match counter {
|
||||
k::DUG => 2.0,
|
||||
k::ROOMS => 3.0,
|
||||
k::WALLS => 2.0,
|
||||
k::TRAPS => 8.0,
|
||||
k::TOWERS => 25.0,
|
||||
k::SPELLS => 5.0,
|
||||
k::SIEGE => 20.0,
|
||||
k::GOLD => 0.02,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
// experience for a slain hero: a share of his bounty
|
||||
pub const HERO_XP: f32 = 0.5;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Talents {
|
||||
pub xp: f32,
|
||||
pub lv: [u8; BRANCHES],
|
||||
}
|
||||
|
||||
// experience the k-th talent point needs in all (the points get dearer)
|
||||
pub fn point_xp(k: u32) -> f32 {
|
||||
(0..k).map(|i| 300.0 + 150.0 * i as f32).sum()
|
||||
}
|
||||
|
||||
impl Talents {
|
||||
pub fn spent(&self) -> u32 {
|
||||
self.lv.iter().map(|&l| l as u32).sum()
|
||||
}
|
||||
// points earned so far (at most enough for every level)
|
||||
pub fn earned(&self) -> u32 {
|
||||
let max = BRANCHES as u32 * MAX_LEVEL as u32;
|
||||
(1..=max).take_while(|&k| self.xp >= point_xp(k)).count() as u32
|
||||
}
|
||||
pub fn free(&self) -> u32 {
|
||||
self.earned().saturating_sub(self.spent())
|
||||
}
|
||||
// progress towards the next point: (experience into it, experience it needs)
|
||||
pub fn progress(&self) -> Option<(f32, f32)> {
|
||||
let k = self.earned();
|
||||
if k >= BRANCHES as u32 * MAX_LEVEL as u32 {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (point_xp(k), point_xp(k + 1));
|
||||
Some((self.xp - a, b - a))
|
||||
}
|
||||
pub fn can_learn(&self, branch: usize) -> bool {
|
||||
branch < BRANCHES && self.lv[branch] < MAX_LEVEL && self.free() > 0
|
||||
}
|
||||
fn level(&self, branch: usize) -> f32 {
|
||||
self.lv.get(branch).copied().unwrap_or(0) as f32
|
||||
}
|
||||
// Builder: imps dig and claim faster, rooms cost less
|
||||
pub fn dig_speed(&self) -> f32 {
|
||||
1.0 + 0.15 * self.level(BUILDER)
|
||||
}
|
||||
pub fn room_cost(&self) -> f32 {
|
||||
1.0 - 0.1 * self.level(BUILDER)
|
||||
}
|
||||
// Warlord: creatures hit harder and take less damage
|
||||
pub fn damage(&self) -> f32 {
|
||||
1.0 + 0.05 * self.level(WARLORD)
|
||||
}
|
||||
pub fn armour(&self) -> f32 {
|
||||
1.0 / (1.0 + 0.05 * self.level(WARLORD))
|
||||
}
|
||||
// Warlock: mana flows faster, spells cost less
|
||||
pub fn mana_rate(&self) -> f32 {
|
||||
1.0 + 0.15 * self.level(WARLOCK)
|
||||
}
|
||||
pub fn spell_cost(&self) -> f32 {
|
||||
1.0 - 0.06 * self.level(WARLOCK)
|
||||
}
|
||||
// Merchant: lower wages, more gold from delivered treasure
|
||||
pub fn wages(&self) -> f32 {
|
||||
1.0 - 0.06 * self.level(MERCHANT)
|
||||
}
|
||||
pub fn treasure(&self) -> f32 {
|
||||
1.0 + 0.06 * self.level(MERCHANT)
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// the talents of the master on `side`
|
||||
pub fn talents_of(&self, side: u8) -> Talents {
|
||||
if side == self.me_side {
|
||||
self.talents.clone()
|
||||
} else {
|
||||
self.masters.iter().find(|m| m.side == side).map(|m| m.talents.clone()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// experience for the master being processed; a new talent point is announced
|
||||
pub(crate) fn add_master_xp(&mut self, xp: f32) {
|
||||
if xp <= 0.0 || self.demo.is_some() {
|
||||
return;
|
||||
}
|
||||
let before = self.talents.earned();
|
||||
self.talents.xp += xp;
|
||||
if self.talents.earned() > before && self.me_side == self.local_side {
|
||||
self.msg("msg.talent_point", vec![]);
|
||||
self.sfx_ui(Sfx::LevelUp);
|
||||
}
|
||||
}
|
||||
|
||||
// the master spends a point on a branch (an order: the same on every computer of a match)
|
||||
pub fn learn_talent(&mut self, branch: usize) {
|
||||
if !self.talents.can_learn(branch) {
|
||||
self.sfx_ui(Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.talents.lv[branch] += 1;
|
||||
let lv = self.talents.lv[branch];
|
||||
self.msg("msg.talent_learned", vec![format!("@{}", NAMES[branch]), lv.to_string()]);
|
||||
self.sfx_ui(Sfx::LevelUp);
|
||||
}
|
||||
|
||||
// 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 };
|
||||
self.talents.lv[b] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn points_get_dearer_and_levels_are_capped() {
|
||||
let mut t = Talents::default();
|
||||
assert_eq!(t.earned(), 0);
|
||||
t.xp = point_xp(1);
|
||||
assert_eq!((t.earned(), t.free()), (1, 1));
|
||||
assert!(point_xp(3) - point_xp(2) > point_xp(2) - point_xp(1));
|
||||
t.xp = 1e9;
|
||||
assert_eq!(t.earned(), 20);
|
||||
t.lv = [5, 5, 5, 5];
|
||||
assert_eq!(t.free(), 0);
|
||||
assert!(!t.can_learn(0) && t.progress().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeds_bring_points_and_points_bring_talents() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
g.mark_rect(cx - 6, cy - 6, cx + 8, cy + 8, true);
|
||||
for _ in 0..(20 * 60 * 6) {
|
||||
g.update(0.05);
|
||||
}
|
||||
assert!(g.talents.xp > 100.0, "digging gives experience: {}", g.talents.xp);
|
||||
g.talents.xp = point_xp(2);
|
||||
let room0 = g.talents.room_cost();
|
||||
g.learn_talent(BUILDER);
|
||||
g.learn_talent(WARLORD);
|
||||
g.learn_talent(WARLORD); // no third point
|
||||
assert_eq!(g.talents.lv, [1, 1, 0, 0]);
|
||||
assert!(g.talents.room_cost() < room0 && g.talents.damage() > 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn talents_make_rooms_cheaper_and_wages_lower() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
for y in cy + 4..cy + 7 {
|
||||
for x in cx..cx + 3 {
|
||||
let t = &mut g.map.t[crate::map::idx(x, y)];
|
||||
t.kind = crate::map::Kind::Floor;
|
||||
t.owner = crate::map::Owner::Player;
|
||||
}
|
||||
}
|
||||
g.gold = 10000.0;
|
||||
let gold0 = g.gold;
|
||||
g.build_rect(1, cx, cy + 4, cx + 2, cy + 6);
|
||||
let plain = gold0 - g.gold;
|
||||
g.sell_rect(cx, cy + 4, cx + 2, cy + 6);
|
||||
g.talents.lv[BUILDER] = 5;
|
||||
let gold1 = g.gold;
|
||||
g.build_rect(1, cx, cy + 4, cx + 2, cy + 6);
|
||||
let cheap = gold1 - g.gold;
|
||||
assert!(cheap < plain * 0.8, "{} < {}", cheap, plain);
|
||||
g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 3);
|
||||
let wages = g.next_payday_cost();
|
||||
g.talents.lv[MERCHANT] = 5;
|
||||
assert!(g.next_payday_cost() < wages);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_masters_spend_their_points() {
|
||||
let mut g = Game::new(9, 64, 64);
|
||||
g.talents.xp = point_xp(6);
|
||||
g.ai_talents(None);
|
||||
assert_eq!(g.talents.free(), 0);
|
||||
assert!(g.talents.lv.iter().all(|&l| l >= 1), "{:?}", g.talents.lv);
|
||||
}
|
||||
}
|
||||
@@ -457,8 +457,11 @@ enum TBtn {
|
||||
Tool(usize),
|
||||
Menu,
|
||||
Overview,
|
||||
Talents, // the master's talent window
|
||||
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
|
||||
}
|
||||
@@ -478,6 +481,7 @@ pub enum UiAction {
|
||||
ChatSetTeam(bool),
|
||||
ChatSend,
|
||||
SendGold(u8, u32), // gold for an ally of a match (side, amount)
|
||||
LearnTalent(u8), // spend a talent point on a branch
|
||||
Consumed,
|
||||
}
|
||||
|
||||
@@ -485,9 +489,11 @@ pub struct Ui {
|
||||
pub tool: Tool,
|
||||
pub show_help: bool,
|
||||
pub show_overview: bool, // list of all creatures
|
||||
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)>,
|
||||
@@ -547,6 +553,28 @@ pub fn wrap(s: &str, cols: usize) -> Vec<String> {
|
||||
out
|
||||
}
|
||||
|
||||
// what a talent branch gives at its level now, and at the next level
|
||||
fn branch_text(t: &crate::talents::Talents, b: usize) -> String {
|
||||
use crate::talents::*;
|
||||
let desc = |lv: u8| {
|
||||
let l = lv as i32;
|
||||
let (a, c) = match b {
|
||||
BUILDER => (15 * l, 10 * l),
|
||||
WARLORD => (5 * l, 5 * l),
|
||||
WARLOCK => (15 * l, 6 * l),
|
||||
_ => (6 * l, 6 * l),
|
||||
};
|
||||
trf(&format!("{}.desc", NAMES[b]), &[a.to_string(), c.to_string()])
|
||||
};
|
||||
let lv = t.lv[b];
|
||||
let now = if lv == 0 { tr("talent.none") } else { desc(lv) };
|
||||
if lv < MAX_LEVEL {
|
||||
format!("{} {}", now, trf("talent.then", &[desc(lv + 1)]))
|
||||
} else {
|
||||
now
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod wrap_tests {
|
||||
#[test]
|
||||
@@ -573,9 +601,11 @@ impl Ui {
|
||||
tool: Tool::Dig,
|
||||
show_help: true,
|
||||
show_overview: false,
|
||||
show_talents: false,
|
||||
coop_end: None,
|
||||
gold_to: 0,
|
||||
ov_scroll: 0,
|
||||
ov_fallen: false,
|
||||
ov_rect: None,
|
||||
show_langsel: false,
|
||||
log: Vec::new(),
|
||||
@@ -664,6 +694,19 @@ impl Ui {
|
||||
TBtn::Overview => {
|
||||
self.open_cat = None;
|
||||
self.show_overview = !self.show_overview;
|
||||
self.show_talents = false;
|
||||
self.ov_scroll = 0;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Talents => {
|
||||
self.open_cat = None;
|
||||
self.show_talents = !self.show_talents;
|
||||
self.show_overview = false;
|
||||
UiAction::Consumed
|
||||
}
|
||||
TBtn::Learn(b) => UiAction::LearnTalent(b),
|
||||
TBtn::OvFallen => {
|
||||
self.ov_fallen = !self.ov_fallen;
|
||||
self.ov_scroll = 0;
|
||||
UiAction::Consumed
|
||||
}
|
||||
@@ -741,7 +784,7 @@ impl Ui {
|
||||
Gap,
|
||||
}
|
||||
let txt = |s: String, c: Color| Part::T(s, c);
|
||||
let cap = g.counts[1].min(30);
|
||||
let cap = g.creature_cap();
|
||||
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT };
|
||||
let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
|
||||
let mut gold_seg = vec![txt(tr("hud.gold"), GOLD), Part::Gap, txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT)];
|
||||
@@ -774,6 +817,13 @@ impl Ui {
|
||||
};
|
||||
segs.insert(3, vec![txt(tr(key), col), Part::Gap, Part::Bar(rv.hp / crate::rival::RIVAL_CORE_HP, col)]);
|
||||
}
|
||||
// the weather above ground (maps with land above ground only)
|
||||
if g.has_sky() {
|
||||
let w = &g.weather;
|
||||
let k = if w.level > 0.05 { w.shown } else { crate::weather::FAIR };
|
||||
let col = [GOLD, Color::new(0.55, 0.7, 1.0, 1.0), Color::new(0.75, 0.78, 0.8, 1.0), Color::new(0.75, 0.6, 1.0, 1.0)][k as usize];
|
||||
segs.push(vec![txt(tr(crate::weather::NAMES[k as usize]), col)]);
|
||||
}
|
||||
if g.waves_on() {
|
||||
segs.push(vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)]);
|
||||
}
|
||||
@@ -891,7 +941,7 @@ impl Ui {
|
||||
let mut popup_top = tb_y;
|
||||
// categories + menu button; a category with several tools opens a row above
|
||||
// (the same layout for mouse and touch; on the desktop the keys still select tools directly)
|
||||
let n = CATS.len() + 2;
|
||||
let n = CATS.len() + 3;
|
||||
let bcols = (((cols - 2) as usize / n) as f32).clamp(4.0, 8.0);
|
||||
let bw = bcols * CW();
|
||||
let x0 = grid_center(sw, bw * n as f32);
|
||||
@@ -932,10 +982,27 @@ impl Ui {
|
||||
let ol: String = ol.chars().take(maxc).collect();
|
||||
gfx.text(&ol, bx + (bw - gfx.measure(&ol, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
||||
self.touch_btns.push((r, TBtn::Overview));
|
||||
// menu button
|
||||
// the master's talents: the frame pulses while points are free
|
||||
let bx = x0 + (CATS.len() + 1) as f32 * bw;
|
||||
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||
let free = g.talents.free();
|
||||
let glow = free > 0 && ((now * 3.0) as i64) % 2 == 0;
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered || self.show_talents { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, if glow { GOLD } else if self.show_talents { FRAME_HI } else { Color::new(0.35, 0.28, 0.2, 1.0) });
|
||||
let isz = 2.0 * CH() - 2.0 * px;
|
||||
draw_texture_ex(&gfx.atlas, bx + (bw - isz) * 0.5, by + 2.0 * px, WHITE, DrawTextureParams { source: Some(Gfx::cell(I_RUNE)), dest_size: Some(vec2(isz, isz)), ..Default::default() });
|
||||
if free > 0 {
|
||||
gfx.text(&free.to_string(), r.x + 3.0 * px, r.y + 3.0 * px, SMALL(), if glow { WHITE } else { GOLD });
|
||||
}
|
||||
let tl = tr("cat.master");
|
||||
let tl: String = tl.chars().take(maxc).collect();
|
||||
gfx.text(&tl, bx + (bw - gfx.measure(&tl, SMALL())) * 0.5, by + 2.0 * CH() + 4.0 * px, SMALL(), TXT);
|
||||
self.touch_btns.push((r, TBtn::Talents));
|
||||
// menu button
|
||||
let bx = x0 + (CATS.len() + 2) as f32 * bw;
|
||||
let r = Rect::new(bx + px, by, bw - 2.0 * px, 3.0 * CH());
|
||||
let hovered = !touch_ui() && r.contains(vec2(mx, my));
|
||||
draw_rectangle(r.x, r.y, r.w, r.h, if hovered { Color::new(0.18, 0.13, 0.1, 1.0) } else { Color::new(0.1, 0.07, 0.07, 1.0) });
|
||||
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0 * px, Color::new(0.35, 0.28, 0.2, 1.0));
|
||||
gfx.glyph('≡', bx + (bw - GW * TITLE()) * 0.5, by + 2.0 * px, TITLE(), FRAME_HI);
|
||||
@@ -1026,6 +1093,42 @@ impl Ui {
|
||||
draw_line(mrx + a.x * ms, mry + a.y * ms, mrx + b.x * ms, mry + b.y * ms, px, WHITE);
|
||||
}
|
||||
}
|
||||
// the camera: a monster's eye on the viewer's side of the footprint, looking in
|
||||
{
|
||||
let near = (corners[2] + corners[3]) * 0.5;
|
||||
let look = vec2(-cam.yaw.sin(), -cam.yaw.cos()); // from the eye towards the target (map units)
|
||||
let r = (4.0 * px).max(ms * 2.5);
|
||||
let (ex, ey) = (mrx + near.x * ms - look.x * r * 1.6, mry + near.y * ms - look.y * r * 1.6);
|
||||
let (ex, ey) = (ex.clamp(mrx + r, mrx + map_w - r), ey.clamp(mry + r, mry + map_h - r));
|
||||
let side = vec2(-look.y, look.x);
|
||||
// the lids: an almond across the view direction
|
||||
let lid = |t: f32, w: f32| vec2(ex, ey) + side * (t * r * 1.25) + look * w;
|
||||
let n = 8;
|
||||
let mut upper = Vec::new();
|
||||
let mut lower = Vec::new();
|
||||
for k in 0..=n {
|
||||
let t = k as f32 / n as f32 * 2.0 - 1.0;
|
||||
let h = (1.0 - t * t).max(0.0).sqrt() * r * 0.75;
|
||||
upper.push(lid(t, h));
|
||||
lower.push(lid(t, -h));
|
||||
}
|
||||
let c = vec2(ex, ey);
|
||||
let sclera = Color::new(0.95, 0.85, 0.35, 1.0);
|
||||
for k in 0..n {
|
||||
draw_triangle(c, upper[k], upper[k + 1], sclera);
|
||||
draw_triangle(c, lower[k], lower[k + 1], sclera);
|
||||
}
|
||||
// the iris looks towards the target, with a slit pupil across it
|
||||
let iris = c + look * (r * 0.2);
|
||||
draw_circle(iris.x, iris.y, r * 0.55, Color::new(0.85, 0.3, 0.08, 1.0));
|
||||
let a = iris + side * (r * 0.45);
|
||||
let b = iris - side * (r * 0.45);
|
||||
draw_line(a.x, a.y, b.x, b.y, (r * 0.22).max(px), Color::new(0.05, 0.0, 0.02, 1.0));
|
||||
for k in 0..n {
|
||||
draw_line(upper[k].x, upper[k].y, upper[k + 1].x, upper[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
||||
draw_line(lower[k].x, lower[k].y, lower[k + 1].x, lower[k + 1].y, px.max(1.0), Color::new(0.2, 0.05, 0.08, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- mission goals (campaign)
|
||||
if let Some(m) = &g.mission {
|
||||
@@ -1168,6 +1271,9 @@ impl Ui {
|
||||
if self.show_overview {
|
||||
self.draw_overview(g, gfx, tb_y);
|
||||
}
|
||||
if self.show_talents {
|
||||
self.draw_talents(g, gfx, tb_y);
|
||||
}
|
||||
|
||||
// ---------------- Tooltip
|
||||
if let Some(i) = hover_tool {
|
||||
@@ -1196,6 +1302,85 @@ impl Ui {
|
||||
}
|
||||
|
||||
// all creatures, grouped by kind: level, mood, wage, job, health; a click jumps there
|
||||
// the master's talents: four branches of five levels, points from experience
|
||||
fn draw_talents(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
|
||||
use crate::talents::*;
|
||||
let (mx, my) = mouse();
|
||||
let t = &g.talents;
|
||||
let cols = ((sw() / CW()) as i32 - 2).min(48);
|
||||
let inner = (cols - 4) as usize;
|
||||
// the lines of each branch: name and level, what it gives now (wrapped)
|
||||
let rows_of = |b: usize| wrap(&branch_text(t, b), inner.saturating_sub(6)).len() as i32 + 2;
|
||||
let rows = 5 + (0..BRANCHES).map(rows_of).sum::<i32>();
|
||||
let x = grid_center(sw(), cols as f32 * CW()).max(0.0);
|
||||
let y = (bottom - rows as f32 * CH() - CH()).max(CH() * 4.0);
|
||||
gfx.window_titled(x, y, cols, rows, S(), false, FRAME_HI, BG, &tr("talent.title"), FRAME_HI);
|
||||
self.rects.push(Rect::new(x, y, cols as f32 * CW(), rows as f32 * CH()));
|
||||
let ix = x + CW() * 2.0;
|
||||
let mut yy = y + CH();
|
||||
// points and the way to the next one
|
||||
gfx.text(&trf("talent.points", &[t.free().to_string()]), ix, yy, S(), if t.free() > 0 { GOLD } else { TXT });
|
||||
yy += CH() * 1.2;
|
||||
match t.progress() {
|
||||
Some((have, need)) => {
|
||||
ascii_bar(gfx, ix, yy, 16, (have / need).clamp(0.0, 1.0), MANA, SMALL());
|
||||
gfx.text(&trf("talent.next", &[(have as i32).to_string(), (need as i32).to_string()]), ix + CW() * 17.0, yy, SMALL(), DIM);
|
||||
}
|
||||
None => gfx.text(&tr("talent.all"), ix, yy, SMALL(), DIM),
|
||||
}
|
||||
yy += CH() * 1.8;
|
||||
for b in 0..BRANCHES {
|
||||
let lv = t.lv[b];
|
||||
let pips: String = (0..MAX_LEVEL).map(|k| if k < lv { '■' } else { '·' }).collect();
|
||||
gfx.text(&format!("{} {}", tr(NAMES[b]), pips), ix, yy, S(), FRAME_HI);
|
||||
// a button to learn the next level
|
||||
if t.can_learn(b) {
|
||||
let r = Rect::new(x + (cols - 7) as f32 * CW(), yy - CH() * 0.2, CW() * 4.0, CH() * 1.6);
|
||||
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, GOLD);
|
||||
gfx.text("+", r.x + (r.w - gfx.measure("+", S())) * 0.5, r.y + (r.h - GH * S()) * 0.5, S(), GOLD);
|
||||
self.touch_btns.push((r, TBtn::Learn(b as u8)));
|
||||
}
|
||||
yy += CH() * 1.2;
|
||||
for l in wrap(&branch_text(t, b), inner.saturating_sub(6)) {
|
||||
gfx.text(&l, ix + CW() * 2.0, yy, SMALL(), DIM);
|
||||
yy += CH();
|
||||
}
|
||||
yy += CH() * 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -1210,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)
|
||||
@@ -1255,8 +1455,9 @@ impl Ui {
|
||||
for e in group {
|
||||
let w = (st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)) as i32;
|
||||
let happy = (e.happy.clamp(0.0, 1.0) * 100.0) as i32;
|
||||
let job: String = tr(job_name(e)).chars().take(16).collect();
|
||||
let line = format!(" {:>2} {:>3}% {:>4}g {:<16}", e.level, happy, w, job);
|
||||
let job: String = tr(job_name(e)).chars().take(12).collect();
|
||||
let name: String = crate::names::display_name(e).chars().take(18).collect();
|
||||
let line = format!(" {:>2} {:<18} {:>3}% {:>4}g {:<12}", e.level, name, happy, w, job);
|
||||
let col = if e.happy > 0.6 { TXT } else if e.happy > 0.25 { GOLD } else { RED };
|
||||
lines.push((line, col, Some(e.id), e.hp / e.max_hp));
|
||||
}
|
||||
@@ -1327,7 +1528,7 @@ impl Ui {
|
||||
let controls: &[&str] = if touch_ui() {
|
||||
&["help.t_tap", "help.t_drag", "help.t_two", "help.t_long", "help.t_double", "help.t_tools"]
|
||||
} else {
|
||||
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
||||
&["help.k_left", "help.k_right", "help.k_slap", "help.k_pan", "help.k_rot", "help.k_reset", "help.k_zoom", "help.k_tools", "help.k_misc", "help.k_misc2"]
|
||||
};
|
||||
let mut keys: Vec<(&str, Color)> = vec![("help.goal", GOLD), ("", TXT)];
|
||||
keys.extend(controls.iter().map(|k| (*k, TXT)));
|
||||
@@ -1631,6 +1832,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
out.push((trf("trader.left", &[(left.ceil() as i32).to_string()]), DIM));
|
||||
return out;
|
||||
}
|
||||
// a named creature: its name is the title
|
||||
if e.name != 0 {
|
||||
out.push((crate::names::display_name(e), GOLD));
|
||||
}
|
||||
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
|
||||
out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD));
|
||||
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
|
||||
@@ -1693,6 +1898,13 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
}
|
||||
let Some(t) = g.map.get(x, y) else { return out };
|
||||
let i = idx(x, y);
|
||||
// a memorial stone: who lies here
|
||||
if let Some(m) = g.memorial_at(i) {
|
||||
let name = crate::names::full_name(m.kind, m.name, m.level);
|
||||
out.push((tr("tile.memorial"), DIM));
|
||||
out.push((trf("tile.memorial_info", &[name, tr(st(m.kind).name), m.level.to_string()]), TXT));
|
||||
return out;
|
||||
}
|
||||
let owner = match t.owner {
|
||||
Owner::Player => "owner.player",
|
||||
Owner::Hero => "owner.hero",
|
||||
@@ -1768,8 +1980,20 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
||||
Kind::Mushroom | Kind::Tree => {
|
||||
let tree = t.kind == Kind::Tree;
|
||||
out.push((tr(if tree { "tile.tree" } else { "tile.mushroom" }), WOOD_C));
|
||||
if t.grown() {
|
||||
if !tree {
|
||||
out.push((tr("tile.mushroom_grown"), GREEN));
|
||||
}
|
||||
let wood = if tree { crate::wood::WOOD_TREE } else { crate::wood::WOOD_MUSHROOM };
|
||||
out.push((trf("tile.plant_info", &[(wood as i32).to_string()]), DIM));
|
||||
} else {
|
||||
// a young mushroom: not yet to be felled
|
||||
out.push((trf("tile.mushroom_growing", &[((t.growth() * 100.0) as i32).to_string()]), DIM));
|
||||
}
|
||||
}
|
||||
Kind::Grove => {
|
||||
out.push((tr("tile.grove"), GREEN));
|
||||
out.push((tr("tile.grove_info"), DIM));
|
||||
}
|
||||
Kind::Obsidian => {
|
||||
out.push((tr("tile.obsidian"), FRAME_HI));
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
// Weather above ground: mostly fair, now and then rain, fog or a storm, each for a few minutes
|
||||
// with soft changes. Rain puts out burning bridges and keeps fire from catching; fog shortens the
|
||||
// sight of everyone outside; a storm brings both rain and lightning that strikes units and trees.
|
||||
// Only maps with land above ground have weather. It runs in the shared simulation (same on every
|
||||
// computer of a match).
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const FAIR: u8 = 0;
|
||||
pub const RAIN: u8 = 1;
|
||||
pub const FOG: u8 = 2;
|
||||
pub const STORM: u8 = 3;
|
||||
pub const NAMES: [&str; 4] = ["weather.fair", "weather.rain", "weather.fog", "weather.storm"];
|
||||
|
||||
// seconds for a change to fade in or out
|
||||
const FADE: f32 = 20.0;
|
||||
// damage of a lightning strike
|
||||
pub const BOLT_DAMAGE: f32 = 40.0;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Weather {
|
||||
pub kind: u8, // the weather that is coming or here
|
||||
pub shown: u8, // the weather that is felt now (changes when the old one has faded out)
|
||||
pub level: f32, // how strong it is: 0 none .. 1 full
|
||||
pub left: f32, // seconds until the next change
|
||||
pub strike_t: f32, // seconds until the next lightning (storm)
|
||||
pub flash: f32, // game time of the last lightning (the sky lights up)
|
||||
}
|
||||
|
||||
// the weather that comes next: about 60% fair, 20% rain, 12% fog, 8% storm
|
||||
fn pick(r: f32) -> u8 {
|
||||
if r < 0.6 {
|
||||
FAIR
|
||||
} else if r < 0.8 {
|
||||
RAIN
|
||||
} else if r < 0.92 {
|
||||
FOG
|
||||
} else {
|
||||
STORM
|
||||
}
|
||||
}
|
||||
|
||||
impl Weather {
|
||||
// strength of an effect: rain falls in rain and storms, fog in fog (a little in storms)
|
||||
pub fn rain(&self) -> f32 {
|
||||
if matches!(self.shown, RAIN | STORM) { self.level } else { 0.0 }
|
||||
}
|
||||
pub fn fog(&self) -> f32 {
|
||||
match self.shown {
|
||||
FOG => self.level,
|
||||
STORM => self.level * 0.4,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
// how much the daylight is dimmed (0 .. 1)
|
||||
pub fn gloom(&self) -> f32 {
|
||||
self.level
|
||||
* match self.shown {
|
||||
RAIN => 0.3,
|
||||
FOG => 0.2,
|
||||
STORM => 0.55,
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn has_sky(&self) -> bool {
|
||||
self.map.sky.iter().any(|&s| s)
|
||||
}
|
||||
|
||||
// it rains on tile i right now (enough to put out fire)
|
||||
pub fn rain_at(&self, i: usize) -> bool {
|
||||
self.weather.rain() > 0.4 && self.map.sky(i)
|
||||
}
|
||||
|
||||
// sight of a unit standing on tile i (fog outside shortens it)
|
||||
pub fn sight_at(&self, i: usize, base: i32) -> i32 {
|
||||
if !self.map.sky(i) {
|
||||
return base;
|
||||
}
|
||||
((base as f32) * (1.0 - 0.5 * self.weather.fog())).round().max(2.0) as i32
|
||||
}
|
||||
|
||||
pub(crate) fn update_weather(&mut self, dt: f32) {
|
||||
if self.map.sky.is_empty() {
|
||||
return;
|
||||
}
|
||||
let w = &mut self.weather;
|
||||
w.left -= dt;
|
||||
if w.left <= 0.0 {
|
||||
// after bad weather it clears up more often
|
||||
let r = self.rng.f();
|
||||
let next = if w.kind != FAIR && r < 0.5 { FAIR } else { pick(self.rng.f()) };
|
||||
let w = &mut self.weather;
|
||||
w.kind = next;
|
||||
w.left = if next == FAIR { 180.0 + self.rng.f() * 240.0 } else { 90.0 + self.rng.f() * 150.0 };
|
||||
let w = &mut self.weather;
|
||||
if w.kind != w.shown || w.level < 0.99 {
|
||||
let key = ["msg.weather_clear", "msg.weather_rain", "msg.weather_fog", "msg.weather_storm"][next as usize];
|
||||
if next != FAIR || w.shown != FAIR {
|
||||
self.tell_local(key, vec![], None);
|
||||
}
|
||||
}
|
||||
}
|
||||
// soft changes: the old weather fades out before the new one fades in
|
||||
let w = &mut self.weather;
|
||||
if w.shown != w.kind {
|
||||
w.level -= dt / FADE;
|
||||
if w.level <= 0.0 {
|
||||
w.level = 0.0;
|
||||
w.shown = w.kind;
|
||||
}
|
||||
} else if w.kind != FAIR {
|
||||
w.level = (w.level + dt / FADE).min(1.0);
|
||||
} else {
|
||||
w.level = (w.level - dt / FADE).max(0.0);
|
||||
}
|
||||
// rain puts out the burning bridges outside
|
||||
if self.weather.rain() > 0.4 && !self.bridgework.burning.is_empty() {
|
||||
let map = &self.map;
|
||||
self.bridgework.burning.retain(|b| !map.sky(b.0));
|
||||
}
|
||||
// lightning in a storm
|
||||
if self.weather.shown == STORM && self.weather.level > 0.6 {
|
||||
self.weather.strike_t -= dt;
|
||||
if self.weather.strike_t <= 0.0 {
|
||||
self.weather.strike_t = 6.0 + self.rng.f() * 14.0;
|
||||
self.lightning_strike();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a bolt from the sky: a unit outside (now and then) or a tree, else bare land
|
||||
pub(crate) fn lightning_strike(&mut self) {
|
||||
let outside: Vec<usize> = (0..self.ents.len()).filter(|&j| {
|
||||
let e = &self.ents[j];
|
||||
e.alive && !e.held && e.captive == 0 && self.map.sky(e.tile())
|
||||
}).collect();
|
||||
let r = self.rng.f();
|
||||
let (x, y) = if r < 0.35 && !outside.is_empty() {
|
||||
let j = outside[(self.rng.next() as usize) % outside.len()];
|
||||
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);
|
||||
}
|
||||
if dead {
|
||||
self.kill(j);
|
||||
}
|
||||
(x, y)
|
||||
} else {
|
||||
let trees: Vec<usize> = (0..self.map.t.len()).filter(|&i| self.map.t[i].kind == Kind::Tree && self.map.sky(i)).collect();
|
||||
if let Some(&i) = trees.get((self.rng.next() as usize) % trees.len().max(1)) {
|
||||
// the tree is split and burns down
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Floor;
|
||||
t.owner = Owner::None;
|
||||
t.marked = false;
|
||||
t.hp = 0.0;
|
||||
let (tx, ty) = xy(i);
|
||||
self.fx(3, tx as f32 + 0.5, ty as f32 + 0.5, 0.0, 0.0, 1.2, 0xff304050);
|
||||
(tx as f32 + 0.5, ty as f32 + 0.5)
|
||||
} else {
|
||||
let sky: Vec<usize> = (0..self.map.t.len()).filter(|&i| self.map.sky(i)).collect();
|
||||
let Some(&i) = sky.get((self.rng.next() as usize) % sky.len().max(1)) else { return };
|
||||
let (tx, ty) = xy(i);
|
||||
(tx as f32 + 0.5, ty as f32 + 0.5)
|
||||
}
|
||||
};
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn surface_game() -> Game {
|
||||
let mut g = Game::new(7, 96, 96);
|
||||
g.add_surface(7, 0.5, 3);
|
||||
assert!(g.has_sky());
|
||||
g
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_weather_changes_and_is_mostly_fair() {
|
||||
let mut g = surface_game();
|
||||
let mut seen = [0usize; 4];
|
||||
for _ in 0..(4 * 60 * 60) {
|
||||
g.update_weather(1.0);
|
||||
seen[g.weather.shown as usize] += 1;
|
||||
}
|
||||
assert!(seen[FAIR as usize] > seen.iter().sum::<usize>() / 3, "{:?}", seen);
|
||||
assert!(seen.iter().filter(|&&n| n > 0).count() >= 3, "several kinds of weather: {:?}", seen);
|
||||
// no weather underground only
|
||||
let mut u = Game::new(7, 96, 96);
|
||||
u.weather.left = 0.0;
|
||||
u.update_weather(1.0);
|
||||
assert_eq!(u.weather.level, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rain_puts_out_fires_and_fog_shortens_sight() {
|
||||
let mut g = surface_game();
|
||||
let i = (0..g.map.t.len()).find(|&i| g.map.sky(i)).unwrap();
|
||||
g.bridgework.burning.push((i, 0.0));
|
||||
g.weather = Weather { kind: RAIN, shown: RAIN, level: 1.0, left: 100.0, ..Default::default() };
|
||||
g.update_weather(0.1);
|
||||
assert!(g.bridgework.burning.is_empty(), "rain puts the fire out");
|
||||
assert!(g.rain_at(i));
|
||||
g.weather = Weather { kind: FOG, shown: FOG, level: 1.0, left: 100.0, ..Default::default() };
|
||||
assert!(g.sight_at(i, 6) < 6);
|
||||
let under = (0..g.map.t.len()).find(|&i| !g.map.sky(i)).unwrap();
|
||||
assert_eq!(g.sight_at(under, 6), 6, "underground nothing changes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storms_strike_with_lightning() {
|
||||
let mut g = surface_game();
|
||||
let trees0 = g.map.t.iter().filter(|t| t.kind == Kind::Tree).count();
|
||||
let i = (0..g.map.t.len()).find(|&i| g.map.sky(i) && g.map.t[i].walkable()).unwrap();
|
||||
let (x, y) = xy(i);
|
||||
let id = g.spawn(EK::Knight, true, x as f32 + 0.5, y as f32 + 0.5, 1);
|
||||
g.weather = Weather { kind: STORM, shown: STORM, level: 1.0, left: 1000.0, ..Default::default() };
|
||||
for _ in 0..30 {
|
||||
g.lightning_strike();
|
||||
}
|
||||
let trees = g.map.t.iter().filter(|t| t.kind == Kind::Tree).count();
|
||||
let hurt = g.ents.iter().find(|e| e.id == id).map_or(true, |e| e.hp < e.max_hp);
|
||||
assert!(trees < trees0 || hurt, "lightning hit something");
|
||||
assert!(g.weather.flash > 0.0 || g.time == 0.0);
|
||||
}
|
||||
}
|
||||
+121
-33
@@ -16,62 +16,106 @@ pub const WOOD_TREE: f32 = 3.0;
|
||||
pub const MUSHROOM_HP: f32 = 4.0;
|
||||
#[allow(dead_code)] // trees come with the surface world
|
||||
pub const TREE_HP: f32 = 6.0;
|
||||
// seconds until a felled giant mushroom grows back
|
||||
pub const REGROW: f32 = 240.0;
|
||||
// seconds until a felled giant mushroom sprouts again, and from sprout to fully grown
|
||||
pub const REGROW: f32 = 90.0;
|
||||
pub const GROW_TIME: f32 = 300.0;
|
||||
// wood a wooden bridge tile, a wooden drawbridge tile and a wooden door cost
|
||||
pub const BRIDGE_WOOD: f32 = 2.0;
|
||||
pub const DRAWBRIDGE_WOOD: f32 = 4.0;
|
||||
pub const DOOR_WOOD: f32 = 3.0;
|
||||
// giant mushrooms in a cavern of a random map: one per this many open tiles
|
||||
const MUSHROOM_SHARE: usize = 70;
|
||||
// mushroom groves of a random map: one per this many map tiles (at least 2, at most 6)
|
||||
const GROVE_SHARE: usize = 2200;
|
||||
const GROVE_RADIUS: i32 = 4;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct WoodWork {
|
||||
pub regrow: Vec<(usize, f32)>, // felled mushroom tile, seconds until it grows back
|
||||
pub regrow: Vec<(usize, f32)>, // felled mushroom tile, seconds until it sprouts again
|
||||
#[serde(default)]
|
||||
pub growing: Vec<usize>, // giant mushrooms still growing (Tile::food: growth left)
|
||||
}
|
||||
|
||||
// a tile where a plant stands alone: all eight neighbours open ground, no other plant next to
|
||||
// it (so a plant never closes a corridor)
|
||||
// open ground of nobody (or a grove's moss)
|
||||
fn open_ground(t: &Tile) -> bool {
|
||||
(t.kind == Kind::Floor && t.owner == Owner::None) || t.kind == Kind::Grove
|
||||
}
|
||||
|
||||
// a tile where a plant may stand: all eight neighbours open, no other plant next to it (so a
|
||||
// plant never closes a corridor)
|
||||
fn lonely_spot(map: &Map, x: i32, y: i32) -> bool {
|
||||
if !inb(x, y) || map.t[idx(x, y)].kind != Kind::Floor || map.t[idx(x, y)].owner != Owner::None {
|
||||
if !inb(x, y) || !open_ground(&map.t[idx(x, y)]) {
|
||||
return false;
|
||||
}
|
||||
DIR8.iter().all(|&(dx, dy)| map.get(x + dx, y + dy).map_or(false, |n| n.walkable() && !n.bridge() && n.kind != Kind::Door))
|
||||
&& (-2..=2).all(|dy| (-2..=2).all(|dx| !map.get(x + dx, y + dy).map_or(false, |n| n.plant())))
|
||||
DIR8.iter().all(|&(dx, dy)| map.get(x + dx, y + dy).map_or(false, |n| n.walkable() && !n.bridge() && n.kind != Kind::Door && !n.plant()))
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// giant mushrooms in the open caverns of a random map (deterministic, from the seed)
|
||||
// mushroom groves in the open caverns of a random map (deterministic, from the seed): mossy
|
||||
// ground nobody can claim, giant mushrooms in a loose grid that grow back when felled
|
||||
pub fn grow_mushrooms(&mut self, seed: u32) {
|
||||
let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9) ^ 0x6d2b_79f5 | 1);
|
||||
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
|
||||
let open: Vec<usize> = (0..self.map.t.len())
|
||||
let open = |g: &Game, x: i32, y: i32| inb(x, y) && !g.map.sky(idx(x, y)) && open_ground(&g.map.t[idx(x, y)]) && !g.map.t[idx(x, y)].bridge();
|
||||
// centres: open ground with open ground all around, away from the heart
|
||||
let r = GROVE_RADIUS;
|
||||
let centres: Vec<usize> = (0..self.map.t.len())
|
||||
.filter(|&i| {
|
||||
let (x, y) = xy(i);
|
||||
(x - cx).abs().max((y - cy).abs()) > 10 && lonely_spot(&self.map, x, y)
|
||||
(x - cx).abs().max((y - cy).abs()) > 12 && (-2..=2).all(|dy| (-2..=2).all(|dx| open(self, x + dx, y + dy)))
|
||||
})
|
||||
.collect();
|
||||
let want = (open.len() / MUSHROOM_SHARE).max(6);
|
||||
let mut placed = 0;
|
||||
let want = (self.map.t.len() / GROVE_SHARE).clamp(2, 6);
|
||||
let mut groves: Vec<(i32, i32)> = Vec::new();
|
||||
let mut tries = 0;
|
||||
while placed < want && tries < want * 20 && !open.is_empty() {
|
||||
while groves.len() < want && tries < 200 && !centres.is_empty() {
|
||||
tries += 1;
|
||||
let i = open[(rng.next() as usize) % open.len()];
|
||||
let (x, y) = xy(i);
|
||||
if self.ents.iter().any(|e| e.alive && e.tile() == i) || !lonely_spot(&self.map, x, y) {
|
||||
let (x, y) = xy(centres[(rng.next() as usize) % centres.len()]);
|
||||
if groves.iter().any(|&(gx, gy)| (gx - x).abs().max((gy - y).abs()) < 3 * r) {
|
||||
continue;
|
||||
}
|
||||
self.plant_mushroom(i);
|
||||
placed += 1;
|
||||
groves.push((x, y));
|
||||
}
|
||||
for &(gx, gy) in &groves {
|
||||
// the moss: the open ground of a rough disc
|
||||
for y in gy - r..=gy + r {
|
||||
for x in gx - r..=gx + r {
|
||||
let d2 = (x - gx).pow(2) + (y - gy).pow(2);
|
||||
if d2 <= r * r + (hash32(x, y) % 3) as i32 - 1 && open(self, x, y) {
|
||||
let t = &mut self.map.t[idx(x, y)];
|
||||
t.kind = Kind::Grove;
|
||||
t.owner = Owner::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// the mushrooms: every other tile in both directions, some still growing
|
||||
for y in gy - r + 1..gy + r {
|
||||
for x in gx - r + 1..gx + r {
|
||||
if (x - gx) % 2 != 0 || (y - gy) % 2 != 0 {
|
||||
continue;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
if self.map.t[i].kind != Kind::Grove || !lonely_spot(&self.map, x, y) || self.ents.iter().any(|e| e.alive && e.tile() == i) {
|
||||
continue;
|
||||
}
|
||||
let left = if rng.f() < 0.6 { 0.0 } else { rng.f() };
|
||||
self.plant_mushroom(i, left);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plant_mushroom(&mut self, i: usize) {
|
||||
// a giant mushroom on tile i; `left`: how much of its growth is still to come (0 = grown)
|
||||
pub fn plant_mushroom(&mut self, i: usize, left: f32) {
|
||||
let grove = self.map.t[i].kind == Kind::Grove || self.map.t[i].res & MUSH_GROVE != 0;
|
||||
let t = &mut self.map.t[i];
|
||||
t.kind = Kind::Mushroom;
|
||||
t.owner = Owner::None;
|
||||
t.hp = MUSHROOM_HP;
|
||||
t.marked = false;
|
||||
t.food = left.clamp(0.0, 1.0);
|
||||
t.res = if grove { MUSH_GROVE } else { 0 };
|
||||
if t.food > 0.0 && !self.woodwork.growing.contains(&i) {
|
||||
self.woodwork.growing.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// an imp felled the plant at tile i: wood for its master, a mushroom grows back later
|
||||
@@ -96,8 +140,23 @@ impl Game {
|
||||
true
|
||||
}
|
||||
|
||||
// the shared step: felled mushrooms grow back where there is room
|
||||
// the shared step: young mushrooms grow, felled ones sprout again where there is room
|
||||
pub(crate) fn update_wood(&mut self, dt: f32) {
|
||||
let mut k = 0;
|
||||
while k < self.woodwork.growing.len() {
|
||||
let i = self.woodwork.growing[k];
|
||||
let t = &mut self.map.t[i];
|
||||
if t.kind != Kind::Mushroom {
|
||||
self.woodwork.growing.swap_remove(k);
|
||||
continue;
|
||||
}
|
||||
t.food = (t.food - dt / GROW_TIME).max(0.0);
|
||||
if t.food <= 0.0 {
|
||||
self.woodwork.growing.swap_remove(k);
|
||||
} else {
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
let mut k = 0;
|
||||
while k < self.woodwork.regrow.len() {
|
||||
self.woodwork.regrow[k].1 -= dt;
|
||||
@@ -108,9 +167,10 @@ impl Game {
|
||||
let i = self.woodwork.regrow[k].0;
|
||||
let (x, y) = xy(i);
|
||||
if lonely_spot(&self.map, x, y) && !self.ents.iter().any(|e| e.alive && e.tile() == i) {
|
||||
self.plant_mushroom(i);
|
||||
// a sprout: it takes GROW_TIME until it can be felled again
|
||||
self.plant_mushroom(i, 1.0);
|
||||
self.woodwork.regrow.swap_remove(k);
|
||||
} else if self.map.t[i].kind != Kind::Floor || self.map.t[i].owner != Owner::None {
|
||||
} else if !open_ground(&self.map.t[i]) {
|
||||
// built over or claimed: it does not come back
|
||||
self.woodwork.regrow.swap_remove(k);
|
||||
} else {
|
||||
@@ -121,37 +181,65 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn hash32(x: i32, y: i32) -> u32 {
|
||||
let h = (x as u32).wrapping_mul(2654435761) ^ (y as u32).wrapping_mul(2246822519);
|
||||
(h ^ (h >> 15)).wrapping_mul(2246822519) >> 7
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn random_maps_have_lonely_giant_mushrooms() {
|
||||
fn random_maps_have_mushroom_groves() {
|
||||
let g = Game::new(9, 96, 96);
|
||||
let groves = g.map.t.iter().filter(|t| t.kind == Kind::Grove).count();
|
||||
let n = g.map.t.iter().filter(|t| t.kind == Kind::Mushroom).count();
|
||||
assert!(groves >= 30, "{} grove tiles", groves);
|
||||
assert!(n >= 6, "{} mushrooms", n);
|
||||
assert!((g.wood - WOOD_START).abs() < 1e-3);
|
||||
// none closes a corridor: every neighbour is open ground
|
||||
// none closes a corridor: every neighbour is open ground; all stand in a grove
|
||||
for i in 0..g.map.t.len() {
|
||||
if g.map.t[i].kind == Kind::Mushroom {
|
||||
let (x, y) = xy(i);
|
||||
assert!(DIR8.iter().all(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |n| n.walkable() || n.plant())));
|
||||
assert!(DIR8.iter().all(|&(dx, dy)| g.map.get(x + dx, y + dy).map_or(false, |n| n.walkable())));
|
||||
assert!(g.map.t[i].res & MUSH_GROVE != 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn felling_gives_wood_and_mushrooms_grow_back() {
|
||||
fn grove_mushrooms_grow_slowly_are_felled_only_when_grown_and_come_back() {
|
||||
let mut g = Game::new(9, 96, 96);
|
||||
let i = g.map.t.iter().position(|t| t.kind == Kind::Mushroom).unwrap();
|
||||
let i = g.map.t.iter().position(|t| t.kind == Kind::Mushroom && t.grown()).unwrap();
|
||||
assert!(g.map.t[i].diggable());
|
||||
let w0 = g.wood;
|
||||
g.felled(i);
|
||||
g.dig_out(i);
|
||||
assert_eq!(g.wood, w0 + WOOD_MUSHROOM);
|
||||
assert_eq!(g.map.t[i].kind, Kind::Floor);
|
||||
g.woodwork.regrow[0].1 = 0.01;
|
||||
assert_eq!(g.map.t[i].kind, Kind::Grove, "the moss stays");
|
||||
// the grove can't be claimed or marked
|
||||
let (x, y) = xy(i);
|
||||
g.mark_rect(x, y, x, y, true);
|
||||
assert!(!g.map.t[i].marked);
|
||||
assert!(!g.claimable(i, 1));
|
||||
// it sprouts again: small, and not to be felled yet
|
||||
let idx_r = g.woodwork.regrow.iter().position(|r| r.0 == i).unwrap();
|
||||
g.woodwork.regrow[idx_r].1 = 0.01;
|
||||
g.update_wood(0.05);
|
||||
assert_eq!(g.map.t[i].kind, Kind::Mushroom, "grown back");
|
||||
assert_eq!(g.map.t[i].kind, Kind::Mushroom, "a sprout");
|
||||
assert!(!g.map.t[i].grown() && !g.map.t[i].diggable());
|
||||
g.mark_rect(x, y, x, y, true);
|
||||
assert!(!g.map.t[i].marked, "a young mushroom can't be marked");
|
||||
// halfway
|
||||
for _ in 0..(GROW_TIME as usize / 2) {
|
||||
g.update_wood(1.0);
|
||||
}
|
||||
assert!(!g.map.t[i].grown() && g.map.t[i].growth() > 0.4);
|
||||
for _ in 0..(GROW_TIME as usize / 2 + 2) {
|
||||
g.update_wood(1.0);
|
||||
}
|
||||
assert!(g.map.t[i].grown() && g.map.t[i].diggable(), "fully grown: ready to fell");
|
||||
assert!(g.spend_wood(3.0) && !g.spend_wood(1000.0));
|
||||
}
|
||||
}
|
||||
@@ -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