diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b6e6cae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - run: cargo build + - run: cargo test -- --test-threads=1 diff --git a/.github/workflows/showcase.yml b/.github/workflows/showcase.yml new file mode 100644 index 0000000..73c0bbe --- /dev/null +++ b/.github/workflows/showcase.yml @@ -0,0 +1,65 @@ +name: Showcase + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + +jobs: + showcase: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo build + - name: Initial Status + run: cargo run -- status + - name: Move Down + run: cargo run -- do move down + - name: Status after Move Down + run: cargo run -- status + - name: Move Left + run: cargo run -- do move left + - name: Status after Move Left + run: cargo run -- status + - name: Move Right + run: cargo run -- do move right + - name: Status after Move Right + run: cargo run -- status + - name: Move Up + run: cargo run -- do move up + - name: Status after Move Up + run: cargo run -- status + - name: Clear Up + run: cargo run -- do clear up + - name: Status after Clear + run: cargo run -- status + - name: Plant Up + run: cargo run -- do plant up + - name: Status after Plant + run: cargo run -- status + - name: Water Up + run: cargo run -- do water up + - name: Status after Water + run: cargo run -- status + - name: Buy Seed + run: cargo run -- do buy seed + - name: Status after Buy + run: cargo run -- status + - name: Harvest Up + run: cargo run -- do harvest up + - name: Status after Harvest + run: cargo run -- status + - name: Sell Strawberry + run: cargo run -- do sell 🍓 + - name: Status after Sell Strawberry + run: cargo run -- status + - name: Sell Mushroom + run: cargo run -- do sell 🍄 + - name: Status after Sell Mushroom + run: cargo run -- status + - name: Fish Up + run: cargo run -- do fish up + - name: Status after Fish + run: cargo run -- status diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml new file mode 100644 index 0000000..5b79c68 --- /dev/null +++ b/.github/workflows/ui.yml @@ -0,0 +1,15 @@ +name: UI + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + +jobs: + ui-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo test initial_farm_ui -- --nocapture diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fab3911 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,792 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "tinydew" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "dirs", + "rand", + "rusqlite", + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[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-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[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-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..bcb7679 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tinydew" +version = "0.1.0" +edition = "2021" + +[dependencies] +rusqlite = { version = "0.31", features = ["bundled"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +clap = { version = "4.5", features = ["derive"] } +rand = "0.8" +dirs = "5.0" +chrono = "0.4" + +[profile.release] +opt-level = 3 +lto = true + +[lib] +name = "tinydew" +path = "src/lib.rs" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..136c642 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,102 @@ +use crate::state::{load_game, save_game}; +use crate::types::Direction; +use crate::ui; +use clap::{Parser, Subcommand}; + +#[derive(Parser)] +#[command(name = "tinydew")] +#[command(version = "0.1.0")] +#[command(about = "A cozy farming and exploration game", long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Status, + Do { + action: String, + arg1: Option, + arg2: Option, + }, +} + +pub fn run() -> Result<(), String> { + let cli = Cli::parse(); + + match cli.command { + Commands::Status => { + let state = load_game(); + println!("{}", ui::render_status(&state)); + } + Commands::Do { action, arg1, arg2 } => { + let mut state = load_game(); + let args: Vec = arg1.into_iter().chain(arg2).collect(); + let _message = execute_action(&mut state, &action, &args); + save_game(&state); + println!("{}", ui::render_status(&state)); + } + } + + Ok(()) +} + +fn execute_action(state: &mut crate::state::GameState, action: &str, args: &[String]) -> String { + let direction = args.first().and_then(|d| match d.to_lowercase().as_str() { + "up" => Some(Direction::Up), + "down" => Some(Direction::Down), + "left" => Some(Direction::Left), + "right" => Some(Direction::Right), + _ => None, + }); + + match action.to_lowercase().as_str() { + "move" => { + if let Some(dir) = direction { + state.try_move(dir) + } else { + "Invalid direction. Use up, down, left, or right.".to_string() + } + } + "water" => { + let dir = direction.unwrap_or(state.player.direction); + state.try_water(dir) + } + "plant" => { + let dir = direction.unwrap_or(state.player.direction); + state.try_plant(dir) + } + "harvest" => { + let dir = direction.unwrap_or(state.player.direction); + state.try_harvest(dir) + } + "clear" => { + let dir = direction.unwrap_or(state.player.direction); + state.try_clear(dir) + } + "fish" => { + let dir = direction.unwrap_or(state.player.direction); + state.try_fish(dir) + } + "buy" => { + if let Some(item) = args.first() { + match item.to_lowercase().as_str() { + "seed" => state.buy_seed(), + _ => format!("Unknown item: {}", item), + } + } else { + "Specify an item to buy.".to_string() + } + } + "sell" => { + if let Some(item) = args.first() { + state.sell_item(item) + } else { + "Specify an item to sell.".to_string() + } + } + "sleep" => state.sleep(), + _ => format!("Unknown action: {}", action), + } +} diff --git a/src/economy.rs b/src/economy.rs new file mode 100644 index 0000000..6cde007 --- /dev/null +++ b/src/economy.rs @@ -0,0 +1,121 @@ +use crate::types::FishType; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Inventory { + pub seeds: u32, + pub produce: HashMap, + pub forage: HashMap, + pub fish: HashMap, +} + +impl Inventory { + pub fn new() -> Self { + Self::default() + } + + pub fn add_produce(&mut self, emoji: &str, qty: u32) { + *self.produce.entry(emoji.to_string()).or_insert(0) += qty; + } + + pub fn remove_produce(&mut self, emoji: &str, qty: u32) -> bool { + if let Some(count) = self.produce.get_mut(emoji) { + if *count >= qty { + *count -= qty; + if *count == 0 { + self.produce.remove(emoji); + } + return true; + } + } + false + } + + pub fn add_forage(&mut self, emoji: &str, qty: u32) { + *self.forage.entry(emoji.to_string()).or_insert(0) += qty; + } + + pub fn remove_forage(&mut self, emoji: &str, qty: u32) -> bool { + if let Some(count) = self.forage.get_mut(emoji) { + if *count >= qty { + *count -= qty; + if *count == 0 { + self.forage.remove(emoji); + } + return true; + } + } + false + } + + pub fn add_fish(&mut self, fish_type: FishType, qty: u32) { + let emoji = fish_type.emoji().to_string(); + *self.fish.entry(emoji).or_insert(0) += qty; + } + + pub fn remove_fish(&mut self, emoji: &str, qty: u32) -> bool { + if let Some(count) = self.fish.get_mut(emoji) { + if *count >= qty { + *count -= qty; + if *count == 0 { + self.fish.remove(emoji); + } + return true; + } + } + false + } + + pub fn has_seeds(&self) -> bool { + self.seeds > 0 + } + + pub fn use_seed(&mut self) -> bool { + if self.seeds > 0 { + self.seeds -= 1; + true + } else { + false + } + } + + pub fn add_seeds(&mut self, qty: u32) { + self.seeds += qty; + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shop { + pub seed_price: u32, + pub produce_prices: HashMap, + pub fish_prices: HashMap, + pub mushroom_price: u32, +} + +impl Shop { + pub fn new() -> Self { + let mut produce_prices = HashMap::new(); + produce_prices.insert("🥕".to_string(), 10); + produce_prices.insert("🍓".to_string(), 15); + produce_prices.insert("🥦".to_string(), 20); + produce_prices.insert("🌺".to_string(), 5); + + let mut fish_prices = HashMap::new(); + fish_prices.insert("🐟".to_string(), 5); + fish_prices.insert("🐠".to_string(), 25); + + Self { + seed_price: 10, + produce_prices, + fish_prices, + mushroom_price: 25, + } + } +} + +impl Default for Shop { + fn default() -> Self { + Self::new() + } +} diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..33c6d0c --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,28 @@ +use crate::types::{Direction, Region}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Player { + pub x: usize, + pub y: usize, + pub region: Region, + pub direction: Direction, +} + +impl Player { + pub fn new(x: usize, y: usize, region: Region) -> Self { + Self { + x, + y, + region, + direction: Direction::Down, + } + } + + pub fn move_to(&mut self, x: usize, y: usize, region: Region, direction: Direction) { + self.x = x; + self.y = y; + self.region = region; + self.direction = direction; + } +} diff --git a/src/festival.rs b/src/festival.rs new file mode 100644 index 0000000..9a01b6d --- /dev/null +++ b/src/festival.rs @@ -0,0 +1,26 @@ +use crate::map::RegionMap; +use crate::season::is_butterfly_festival; +use crate::types::{Region, TileType}; +use std::collections::HashMap; + +pub fn update_festival_state(maps: &mut HashMap, day: u32) { + if is_butterfly_festival(day) { + if let Some(square) = maps.get_mut(&Region::Square) { + square.set(2, 2, TileType::Wonder); + } + } else if let Some(square) = maps.get_mut(&Region::Square) { + if let Some(tile) = get_square_wonder_tile() { + if matches!(square.get(2, 2), Some(TileType::Wonder)) { + square.set(2, 2, tile); + } + } + } +} + +fn get_square_wonder_tile() -> Option { + Some(TileType::Grass) +} + +pub fn handle_wonder_message() -> &'static str { + "That is so beautiful. Let's enjoy it together in the game." +} diff --git a/src/fishing.rs b/src/fishing.rs new file mode 100644 index 0000000..4475829 --- /dev/null +++ b/src/fishing.rs @@ -0,0 +1,71 @@ +use crate::map::RegionMap; +use crate::types::{FishType, Region, TileType}; +use rand::{Rng, SeedableRng}; + +#[derive(Debug, Clone)] +pub struct FishResult { + pub caught: Option, + pub message: String, +} + +pub fn roll_fish(seed: u64) -> Option { + let mut rng = rand::rngs::StdRng::seed_from_u64(seed); + let roll = rng.gen_range(0..100); + + if roll < 60 { + Some(FishType::Common) + } else if roll < 80 { + Some(FishType::Rare) + } else { + None + } +} + +pub fn try_fish(map: &mut RegionMap, x: usize, y: usize, seed: u64) -> FishResult { + if let Some(tile) = map.get(x, y) { + if matches!(tile, TileType::River) { + map.set(x, y, TileType::RiverBubble); + + if let Some(fish) = roll_fish(seed) { + FishResult { + caught: Some(fish), + message: format!("You caught a {}!", fish.emoji()), + } + } else { + FishResult { + caught: None, + message: "No bite...".to_string(), + } + } + } else if matches!(tile, TileType::RiverBubble) { + FishResult { + caught: None, + message: "The bubble pops but there's nothing here...".to_string(), + } + } else { + FishResult { + caught: None, + message: "You can't fish here.".to_string(), + } + } + } else { + FishResult { + caught: None, + message: "You can't fish here.".to_string(), + } + } +} + +pub fn reset_river_bubbles(maps: &mut HashMap) { + for (_, map) in maps.iter_mut() { + for y in 0..map.height { + for x in 0..map.width { + if matches!(map.get(x, y), Some(TileType::RiverBubble)) { + map.set(x, y, TileType::River); + } + } + } + } +} + +use std::collections::HashMap; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b4e2a12 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,110 @@ +pub mod cli; +pub mod economy; +pub mod entity; +pub mod festival; +pub mod fishing; +pub mod map; +pub mod season; +pub mod spawn; +pub mod state; +pub mod time; +pub mod types; +pub mod ui; +pub mod weather; + +#[cfg(test)] +mod tests { + use crate::state::GameState; + use crate::types::{Direction, Region}; + use crate::ui; + + #[test] + fn initial_farm_ui() { + let state = GameState::new(); + let output = ui::render_status(&state); + assert!(output.contains("tinydew day 1")); + assert!(output.contains("🌿")); + assert!(output.contains("🧑")); + assert!(output.contains("Money: 💰 $100")); + } + + #[test] + fn test_movement() { + let mut state = GameState::new(); + let result = state.try_move(Direction::Right); + assert!(state.player.x != 6 || result.contains("EastPath")); + } + + #[test] + fn test_planting() { + let mut state = GameState::new(); + state.try_move(Direction::Up); + state.try_clear(Direction::Up); + let result = state.try_plant(Direction::Up); + assert!(result.contains("Planted")); + assert_eq!(state.inventory.seeds, 4); + } + + #[test] + fn test_watering() { + let mut state = GameState::new(); + state.try_move(Direction::Up); + state.try_clear(Direction::Up); + state.try_plant(Direction::Up); + let result = state.try_water(Direction::Up); + assert!(result.contains("Watered")); + } + + #[test] + fn test_buy_seed() { + let mut state = GameState::new(); + let result = state.buy_seed(); + assert!(result.contains("Bought")); + assert_eq!(state.money, 90); + assert_eq!(state.inventory.seeds, 6); + } + + #[test] + fn test_sell_item() { + let mut state = GameState::new(); + state.inventory.add_produce("🍓", 1); + let result = state.sell_item("🍓"); + assert!(result.contains("Sold")); + assert_eq!(state.money, 115); + } + + #[test] + fn test_sleep() { + let mut state = GameState::new(); + state.time.advance(60); + let result = state.sleep(); + assert!(result.contains("Day 2")); + assert_eq!(state.player.region, crate::types::Region::Farm); + } + + #[test] + fn test_region_transition() { + let mut state = GameState::new(); + state.player.x = 6; + state.player.y = 5; + state.try_move(Direction::Right); + assert_eq!(state.player.region, Region::EastPath); + } + + #[test] + fn test_time_advance() { + let mut state = GameState::new(); + state.advance_time(30); + assert_eq!(state.time.minutes, 390); + state.advance_time(1050); + assert_eq!(state.time.day, 2); + } + + #[test] + fn test_day_transition() { + let mut state = GameState::new(); + state.process_day_transition(); + assert_eq!(state.time.day, 2); + assert_eq!(state.time.minutes, 360); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..41065bb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +use tinydew::cli; + +fn main() { + if let Err(e) = cli::run() { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} diff --git a/src/map.rs b/src/map.rs new file mode 100644 index 0000000..1b56907 --- /dev/null +++ b/src/map.rs @@ -0,0 +1,189 @@ +use crate::types::{Direction, FlowerState, Region, TileType}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tile { + pub tile_type: TileType, +} + +impl Tile { + pub fn new(tile_type: TileType) -> Self { + Self { tile_type } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegionMap { + pub region: Region, + pub width: usize, + pub height: usize, + pub tiles: Vec>, +} + +impl RegionMap { + pub fn new(region: Region, width: usize, height: usize) -> Self { + let tiles = vec![vec![TileType::Grass; width]; height]; + Self { + region, + width, + height, + tiles, + } + } + + pub fn get(&self, x: usize, y: usize) -> Option { + if x < self.width && y < self.height { + Some(self.tiles[y][x]) + } else { + None + } + } + + pub fn set(&mut self, x: usize, y: usize, tile: TileType) { + if x < self.width && y < self.height { + self.tiles[y][x] = tile; + } + } + + pub fn is_walkable(&self, x: usize, y: usize) -> bool { + self.get(x, y).map(|t| t.is_walkable()).unwrap_or(false) + } +} + +pub fn create_farm_map() -> RegionMap { + let mut map = RegionMap::new(Region::Farm, 8, 8); + + for x in 0..8 { + map.set(x, 0, TileType::Boundary); + map.set(x, 7, TileType::Boundary); + } + for y in 0..8 { + map.set(0, y, TileType::Boundary); + map.set(7, y, TileType::Boundary); + } + + map.set(2, 2, TileType::House); + map.set(7, 5, TileType::PathEast); + + map +} + +pub fn create_east_path_map() -> RegionMap { + let mut map = RegionMap::new(Region::EastPath, 11, 4); + + for x in 0..11 { + map.set(x, 0, TileType::Boundary); + map.set(x, 3, TileType::Boundary); + } + for y in 0..4 { + map.set(10, y, TileType::Boundary); + } + + map.set(0, 2, TileType::PathFarm); + map.set(5, 0, TileType::PathSquare); + map.set(2, 3, TileType::PathSouthRiver); + map.set(9, 2, TileType::Mushroom); + + map +} + +pub fn create_square_map() -> RegionMap { + let mut map = RegionMap::new(Region::Square, 9, 5); + + for x in 0..9 { + map.set(x, 0, TileType::Boundary); + map.set(x, 4, TileType::Boundary); + } + for y in 0..5 { + map.set(0, y, TileType::Boundary); + map.set(8, y, TileType::Boundary); + } + + map.set(4, 2, TileType::Fountain); + map.set(1, 1, TileType::Flower(FlowerState { mature: true })); + map.set(4, 4, TileType::PathSquare); + + map +} + +pub fn create_south_river_map() -> RegionMap { + let mut map = RegionMap::new(Region::SouthRiver, 13, 4); + + for x in 0..13 { + map.set(x, 0, TileType::Boundary); + } + for y in 2..4 { + for x in 0..13 { + map.set(x, y, TileType::River); + } + } + + map.set(2, 0, TileType::PathSouthRiverGate); + + map +} + +pub fn create_initial_maps() -> HashMap { + let mut maps = HashMap::new(); + maps.insert(Region::Farm, create_farm_map()); + maps.insert(Region::EastPath, create_east_path_map()); + maps.insert(Region::Square, create_square_map()); + maps.insert(Region::SouthRiver, create_south_river_map()); + maps +} + +pub struct TransitionResult { + pub new_region: Region, + pub new_x: usize, + pub new_y: usize, + pub new_direction: Direction, +} + +#[allow(dead_code)] +pub fn get_transition( + from_region: Region, + from_x: usize, + from_y: usize, + _direction: Direction, +) -> Option { + match (from_region, from_x, from_y) { + (Region::Farm, 7, 5) => Some(TransitionResult { + new_region: Region::EastPath, + new_x: 1, + new_y: 2, + new_direction: Direction::Right, + }), + (Region::EastPath, 0, 2) => Some(TransitionResult { + new_region: Region::Farm, + new_x: 6, + new_y: 5, + new_direction: Direction::Left, + }), + (Region::EastPath, 5, 0) => Some(TransitionResult { + new_region: Region::Square, + new_x: 4, + new_y: 3, + new_direction: Direction::Up, + }), + (Region::Square, 4, 4) => Some(TransitionResult { + new_region: Region::EastPath, + new_x: 5, + new_y: 1, + new_direction: Direction::Down, + }), + (Region::EastPath, 2, 3) => Some(TransitionResult { + new_region: Region::SouthRiver, + new_x: 2, + new_y: 1, + new_direction: Direction::Down, + }), + (Region::SouthRiver, 2, 0) => Some(TransitionResult { + new_region: Region::EastPath, + new_x: 2, + new_y: 2, + new_direction: Direction::Up, + }), + _ => None, + } +} diff --git a/src/season.rs b/src/season.rs new file mode 100644 index 0000000..bd89655 --- /dev/null +++ b/src/season.rs @@ -0,0 +1,37 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Season { + Spring, + Summer, + Fall, + Winter, +} + +impl Season { + pub fn from_day(day: u32) -> Self { + let day_in_year = ((day - 1) % 112) + 1; + if day_in_year <= 28 { + Season::Spring + } else if day_in_year <= 56 { + Season::Summer + } else if day_in_year <= 84 { + Season::Fall + } else { + Season::Winter + } + } +} + +pub fn is_butterfly_festival(day: u32) -> bool { + let day_in_year = ((day - 1) % 112) + 1; + Season::from_day(day) == Season::Spring && day_in_year == 28 +} + +pub fn get_festival_message(day: u32) -> Option { + if is_butterfly_festival(day) { + Some("Today is Butterfly Festival, enjoy it!".to_string()) + } else { + None + } +} diff --git a/src/spawn.rs b/src/spawn.rs new file mode 100644 index 0000000..d014889 --- /dev/null +++ b/src/spawn.rs @@ -0,0 +1,75 @@ +use crate::map::RegionMap; +use crate::types::{FlowerState, Region, TileType}; +use rand::{Rng, SeedableRng}; + +pub fn get_valid_spawn_tiles(map: &RegionMap) -> Vec<(usize, usize)> { + let mut valid = Vec::new(); + for y in 0..map.height { + for x in 0..map.width { + if let Some(tile) = map.get(x, y) { + if matches!(tile, TileType::Grass) + && !(map.region == Region::Square && (x == 4 && y == 2)) + && !(map.region == Region::Farm && (x == 2 && y == 2)) + { + valid.push((x, y)); + } + } + } + } + valid +} + +pub fn spawn_daily_flowers( + maps: &mut HashMap, + day: u32, + seed: u64, +) -> Vec<(Region, usize, usize)> { + let mut spawned = Vec::new(); + let mut rng = rand::rngs::StdRng::seed_from_u64(seed.wrapping_mul(day as u64)); + + for (®ion, map) in maps.iter_mut() { + let has_flower = (0..map.height) + .any(|y| (0..map.width).any(|x| matches!(map.get(x, y), Some(TileType::Flower(_))))); + + if !has_flower { + let valid_tiles = get_valid_spawn_tiles(map); + if !valid_tiles.is_empty() { + let idx = rng.gen_range(0..valid_tiles.len()); + let (x, y) = valid_tiles[idx]; + map.set(x, y, TileType::Flower(FlowerState { mature: false })); + spawned.push((region, x, y)); + } + } + } + + spawned +} + +pub fn spawn_daily_mushrooms( + maps: &mut HashMap, + day: u32, + seed: u64, +) -> Vec<(Region, usize, usize)> { + let mut spawned = Vec::new(); + let mut rng = + rand::rngs::StdRng::seed_from_u64(seed.wrapping_mul(day as u64).wrapping_add(1000)); + + for (®ion, map) in maps.iter_mut() { + let has_mushroom = (0..map.height) + .any(|y| (0..map.width).any(|x| matches!(map.get(x, y), Some(TileType::Mushroom)))); + + if !has_mushroom && region != Region::SouthRiver { + let valid_tiles = get_valid_spawn_tiles(map); + if !valid_tiles.is_empty() { + let idx = rng.gen_range(0..valid_tiles.len()); + let (x, y) = valid_tiles[idx]; + map.set(x, y, TileType::Mushroom); + spawned.push((region, x, y)); + } + } + } + + spawned +} + +use std::collections::HashMap; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..5f3042c --- /dev/null +++ b/src/state.rs @@ -0,0 +1,671 @@ +use crate::economy::{Inventory, Shop}; +use crate::entity::Player; +use crate::fishing::reset_river_bubbles; +use crate::map::{RegionMap, create_initial_maps, get_transition}; +use crate::season::get_festival_message; +use crate::spawn::{spawn_daily_flowers, spawn_daily_mushrooms}; +use crate::time::GameTime; +use crate::types::{CropType, Direction, FlowerState, Region, TileType, Weather}; +use crate::weather::roll_weather; +use rand::{Rng, SeedableRng}; +use rusqlite::{Connection, params}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CropData { + pub x: usize, + pub y: usize, + pub crop_type: CropType, + pub days_grown: u32, + pub watered: bool, +} + +#[derive(Debug, Clone, Default)] +pub struct CropsMap(HashMap<(Region, usize, usize), CropData>); + +impl CropsMap { + pub fn get(&self, key: &(Region, usize, usize)) -> Option<&CropData> { + self.0.get(key) + } + + pub fn get_mut(&mut self, key: &(Region, usize, usize)) -> Option<&mut CropData> { + self.0.get_mut(key) + } + + pub fn contains_key(&self, key: &(Region, usize, usize)) -> bool { + self.0.contains_key(key) + } + + pub fn insert(&mut self, key: (Region, usize, usize), value: CropData) { + self.0.insert(key, value); + } + + pub fn remove(&mut self, key: &(Region, usize, usize)) -> Option { + self.0.remove(key) + } + + pub fn iter_mut( + &mut self, + ) -> std::collections::hash_map::IterMut<'_, (Region, usize, usize), CropData> { + self.0.iter_mut() + } +} + +impl Serialize for CropsMap { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(self.0.len()))?; + for ((region, x, y), data) in &self.0 { + let key = format!("{:?}-{}-{}", region, x, y); + map.serialize_entry(&key, data)?; + } + map.end() + } +} + +impl<'de> Deserialize<'de> for CropsMap { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map = HashMap::::deserialize(deserializer)?; + let mut crops = CropsMap::default(); + for (key, data) in map { + let parts: Vec<&str> = key.splitn(2, '-').collect(); + if parts.len() == 2 { + let region = match parts[0] { + "Farm" => Region::Farm, + "EastPath" => Region::EastPath, + "Square" => Region::Square, + "SouthRiver" => Region::SouthRiver, + _ => continue, + }; + let coords: Vec<&str> = parts[1].split('-').collect(); + if coords.len() == 2 { + if let (Ok(x), Ok(y)) = (coords[0].parse::(), coords[1].parse::()) + { + crops.0.insert((region, x, y), data); + } + } + } + } + Ok(crops) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameState { + pub time: GameTime, + pub weather: Weather, + pub player: Player, + pub money: u32, + pub inventory: Inventory, + pub maps: HashMap, + pub crops: CropsMap, + pub bottom_message: String, + pub is_new_game: bool, +} + +impl Default for GameState { + fn default() -> Self { + Self::new() + } +} + +impl GameState { + pub fn new() -> Self { + let maps = create_initial_maps(); + let weather = Weather::Sunny; + let time = GameTime::start_of_day(1); + + let player = Player::new(6, 5, Region::Farm); + + let mut state = Self { + time, + weather, + player, + money: 100, + inventory: Inventory::new(), + maps, + crops: CropsMap::default(), + bottom_message: "Welcome to TinyDew!".to_string(), + is_new_game: true, + }; + + state.inventory.add_seeds(5); + state + } + + pub fn get_tile(&self, region: Region, x: usize, y: usize) -> Option { + if let Some(crop_data) = self.crops.get(&(region, x, y)) { + return Some(TileType::Crop(crop_data.crop_type, crop_data.is_mature())); + } + self.maps.get(®ion).and_then(|m| m.get(x, y)) + } + + pub fn get_current_map(&self) -> Option<&RegionMap> { + self.maps.get(&self.player.region) + } + + pub fn get_current_map_mut(&mut self) -> Option<&mut RegionMap> { + self.maps.get_mut(&self.player.region) + } + + pub fn process_day_transition(&mut self) { + let new_day = self.time.day + 1; + self.time.day = new_day; + self.time.minutes = 6 * 60; + + self.weather = roll_weather(new_day, new_day as u64); + + for (_, crop) in self.crops.iter_mut() { + if crop.watered { + crop.days_grown += 1; + } + crop.watered = false; + } + + reset_river_bubbles(&mut self.maps); + + let seed = (new_day as u64).wrapping_mul(12345); + spawn_daily_flowers(&mut self.maps, new_day, seed); + spawn_daily_mushrooms(&mut self.maps, new_day, seed); + + crate::festival::update_festival_state(&mut self.maps, new_day); + + if let Some(msg) = get_festival_message(new_day) { + self.bottom_message = msg; + } else { + self.bottom_message = format!("Good morning! Day {}.", new_day); + } + } + + pub fn try_move(&mut self, direction: Direction) -> String { + let (dx, dy) = direction.delta(); + let new_x = self.player.x as i32 + dx; + let new_y = self.player.y as i32 + dy; + + if new_x < 0 || new_y < 0 { + return "You can't go that way.".to_string(); + } + + let new_x = new_x as usize; + let new_y = new_y as usize; + + if let Some(map) = self.get_current_map() { + if !map.is_walkable(new_x, new_y) { + if let Some(tile) = map.get(new_x, new_y) { + if matches!(tile, TileType::Crop(_, true)) + || matches!(tile, TileType::Flower(FlowerState { mature: true })) + { + return "There's a mature crop here. Harvest it first.".to_string(); + } + if matches!(tile, TileType::Wonder) { + self.player.x = new_x; + self.player.y = new_y; + self.player.direction = direction; + self.advance_time(5); + return crate::festival::handle_wonder_message().to_string(); + } + } + return "You can't walk there.".to_string(); + } + } + + self.player.x = new_x; + self.player.y = new_y; + self.player.direction = direction; + self.advance_time(5); + + if let Some(transition) = + get_transition(self.player.region, self.player.x, self.player.y, direction) + { + self.player.region = transition.new_region; + self.player.x = transition.new_x; + self.player.y = transition.new_y; + self.player.direction = transition.new_direction; + self.bottom_message = format!("Entered {}.", self.player.region); + return self.bottom_message.clone(); + } + + self.bottom_message = "You move.".to_string(); + self.bottom_message.clone() + } + + pub fn try_water(&mut self, direction: Direction) -> String { + let (dx, dy) = direction.delta(); + let x = self.player.x as i32 + dx; + let y = self.player.y as i32 + dy; + + if x < 0 || y < 0 { + return "Nothing to water there.".to_string(); + } + + let x = x as usize; + let y = y as usize; + + if let Some(crop_data) = self.crops.get_mut(&(self.player.region, x, y)) { + crop_data.watered = true; + self.advance_time(5); + self.bottom_message = "Watered the crop.".to_string(); + return self.bottom_message.clone(); + } + + "Nothing to water there.".to_string() + } + + pub fn try_plant(&mut self, direction: Direction) -> String { + if self.player.region != Region::Farm { + return "You can't plant here.".to_string(); + } + + if !self.inventory.has_seeds() { + return "No seeds available.".to_string(); + } + + let (dx, dy) = direction.delta(); + let x = self.player.x as i32 + dx; + let y = self.player.y as i32 + dy; + + if x < 0 || y < 0 { + return "Can't plant there.".to_string(); + } + + let x = x as usize; + let y = y as usize; + + if let Some(map) = self.get_current_map() { + if let Some(tile) = map.get(x, y) { + match tile { + TileType::Grass | TileType::Soil => { + if self.crops.contains_key(&(self.player.region, x, y)) { + return "Something is already planted here.".to_string(); + } + + self.inventory.use_seed(); + + let mut rng = rand::rngs::StdRng::from_entropy(); + let roll = rng.gen_range(0..4); + let crop_type = match roll { + 0 => CropType::Carrot, + 1 => CropType::Strawberry, + 2 => CropType::Cauliflower, + _ => CropType::Flower, + }; + + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Soil); + } + + self.crops.insert( + (self.player.region, x, y), + CropData { + x, + y, + crop_type, + days_grown: 0, + watered: false, + }, + ); + + self.advance_time(5); + self.bottom_message = format!("Planted a {}!", crop_type.produce_emoji()); + return self.bottom_message.clone(); + } + _ => return "Can't plant here.".to_string(), + } + } + } + + "Can't plant here.".to_string() + } + + pub fn try_harvest(&mut self, direction: Direction) -> String { + let (dx, dy) = direction.delta(); + let x = self.player.x as i32 + dx; + let y = self.player.y as i32 + dy; + + if x < 0 || y < 0 { + return "Nothing to harvest.".to_string(); + } + + let x = x as usize; + let y = y as usize; + + if self.player.region == Region::Square { + if let Some(map) = self.get_current_map() { + if let Some(TileType::Flower(FlowerState { mature: true })) = map.get(x, y) { + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Grass); + } + self.inventory.add_produce("🌺", 1); + self.advance_time(5); + self.bottom_message = "Harvested a flower!".to_string(); + return self.bottom_message.clone(); + } + } + } + + if self.player.region == Region::EastPath { + if let Some(map) = self.get_current_map() { + if let Some(TileType::Mushroom) = map.get(x, y) { + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Grass); + } + self.inventory.add_forage("🍄", 1); + self.advance_time(5); + self.bottom_message = "Harvested a mushroom!".to_string(); + return self.bottom_message.clone(); + } + } + } + + if let Some(crop_data) = self.crops.get(&(self.player.region, x, y)) { + if crop_data.is_mature() { + let _crop_type = crop_data.crop_type; + let emoji = crop_data.crop_type.produce_emoji().to_string(); + + self.crops.remove(&(self.player.region, x, y)); + + if let Some(map) = self.get_current_map() { + if matches!(map.get(x, y), Some(TileType::Crop(_, _))) { + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Grass); + } + } + } + + self.inventory.add_produce(&emoji, 1); + self.advance_time(5); + self.bottom_message = format!("Harvested {}!", emoji); + return self.bottom_message.clone(); + } else { + return "This crop isn't ready yet.".to_string(); + } + } + + "Nothing to harvest.".to_string() + } + + pub fn try_clear(&mut self, direction: Direction) -> String { + if self.player.region == Region::Square { + return "You can't clear here.".to_string(); + } + + if self.player.region == Region::EastPath { + return "You can't clear here.".to_string(); + } + + let (dx, dy) = direction.delta(); + let x = self.player.x as i32 + dx; + let y = self.player.y as i32 + dy; + + if x < 0 || y < 0 { + return "Can't clear there.".to_string(); + } + + let x = x as usize; + let y = y as usize; + + if let Some(map) = self.get_current_map() { + if let Some(tile) = map.get(x, y) { + if self.crops.contains_key(&(self.player.region, x, y)) { + return "There's a crop here. Harvest it first.".to_string(); + } + + match tile { + TileType::Grass => { + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Soil); + } + self.advance_time(5); + self.bottom_message = "Cleared the tile.".to_string(); + return self.bottom_message.clone(); + } + TileType::Soil => { + if let Some(map_mut) = self.get_current_map_mut() { + map_mut.set(x, y, TileType::Grass); + } + self.advance_time(5); + self.bottom_message = "Cleared the tile.".to_string(); + return self.bottom_message.clone(); + } + _ => return "Can't clear this tile.".to_string(), + } + } + } + + "Can't clear this tile.".to_string() + } + + pub fn try_fish(&mut self, direction: Direction) -> String { + if self.player.region != Region::SouthRiver { + return "You can only fish in the river.".to_string(); + } + + let (dx, dy) = direction.delta(); + let x = self.player.x as i32 + dx; + let y = self.player.y as i32 + dy; + + if x < 0 || y < 0 { + return "You can't fish there.".to_string(); + } + + let x = x as usize; + let y = y as usize; + + let seed = self.time.day as u64 * 1000 + self.time.minutes as u64; + let result = crate::fishing::try_fish(self.get_current_map_mut().unwrap(), x, y, seed); + + if let Some(fish) = result.caught { + self.inventory.add_fish(fish, 1); + } + + self.advance_time(10); + self.bottom_message = result.message; + self.bottom_message.clone() + } + + pub fn buy_seed(&mut self) -> String { + let shop = Shop::new(); + if self.money >= shop.seed_price { + self.money -= shop.seed_price; + self.inventory.add_seeds(1); + self.bottom_message = "Bought a seed.".to_string(); + self.bottom_message.clone() + } else { + "Not enough money.".to_string() + } + } + + pub fn sell_item(&mut self, emoji: &str) -> String { + let shop = Shop::new(); + + if let Some(price) = shop.produce_prices.get(emoji) { + if self.inventory.remove_produce(emoji, 1) { + self.money += price; + self.bottom_message = format!("Sold {} for ${}!", emoji, price); + return self.bottom_message.clone(); + } + return "You don't have any of those.".to_string(); + } + + if let Some(price) = shop.fish_prices.get(emoji) { + if self.inventory.remove_fish(emoji, 1) { + self.money += price; + self.bottom_message = format!("Sold {} for ${}!", emoji, price); + return self.bottom_message.clone(); + } + return "You don't have any of those.".to_string(); + } + + if emoji == "🍄" { + if self.inventory.remove_forage("🍄", 1) { + self.money += shop.mushroom_price; + self.bottom_message = format!("Sold 🍄 for ${}!", shop.mushroom_price); + return self.bottom_message.clone(); + } + return "You don't have any mushrooms.".to_string(); + } + + "You can't sell that.".to_string() + } + + pub fn sleep(&mut self) -> String { + if let Some(map) = self.get_current_map() { + if !matches!(map.get(self.player.x, self.player.y), Some(TileType::House)) + && self.player.region != Region::Farm + { + return "You can only sleep in your house.".to_string(); + } + } + + self.process_day_transition(); + + self.player.region = Region::Farm; + self.player.x = 3; + self.player.y = 3; + + self.bottom_message = format!("Good morning! Day {}.", self.time.day); + self.bottom_message.clone() + } + + pub fn advance_time(&mut self, minutes: u32) { + self.time.advance(minutes); + } + + pub fn get_greeting(&self) -> String { + if let Some(msg) = get_festival_message(self.time.day) { + return msg; + } + + let hour = self.time.hour(); + let weather_msg = match self.weather { + Weather::Sunny => "The sun is shining.", + Weather::Cloudy => "It's a cloudy day.", + Weather::Rainy => "It's raining.", + }; + + if self.time.is_night() { + if hour >= 20 { + "Night has fallen. Try sleeping until morning.".to_string() + } else { + "It's getting late. You should rest soon.".to_string() + } + } else if hour < 12 { + format!("Good morning! {}", weather_msg) + } else if hour < 17 { + format!("Good afternoon! {}", weather_msg) + } else { + format!("Good evening! {}", weather_msg) + } + } +} + +impl CropData { + pub fn is_mature(&self) -> bool { + self.days_grown >= self.crop_type.maturity_days() + } +} + +pub struct GameDatabase { + conn: Connection, +} + +impl GameDatabase { + pub fn new(path: &PathBuf) -> Result { + let conn = Connection::open(path)?; + + conn.execute_batch( + "PRAGMA journal_mode=WAL; + PRAGMA synchronous=NORMAL; + CREATE TABLE IF NOT EXISTS schema_version ( + id INTEGER PRIMARY KEY CHECK (id = 1), + version INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS game_save ( + id INTEGER PRIMARY KEY CHECK (id = 1), + updated_at TEXT NOT NULL, + payload TEXT NOT NULL + )", + )?; + + let version: i64 = conn + .query_row( + "SELECT version FROM schema_version WHERE id = 1", + [], + |row| row.get(0), + ) + .unwrap_or(1); + + if version < 1 { + conn.execute( + "INSERT OR REPLACE INTO schema_version (id, version) VALUES (1, 1)", + [], + )?; + } + + Ok(Self { conn }) + } + + pub fn load(&self) -> Option { + let payload: String = self + .conn + .query_row("SELECT payload FROM game_save WHERE id = 1", [], |row| { + row.get(0) + }) + .ok()?; + + serde_json::from_str(&payload).ok() + } + + pub fn save(&self, state: &GameState) -> Result<(), rusqlite::Error> { + let payload = serde_json::to_string(state).unwrap(); + let now = chrono::Utc::now().to_rfc3339(); + + self.conn.execute( + "INSERT OR REPLACE INTO game_save (id, updated_at, payload) VALUES (1, ?1, ?2)", + params![now, payload], + )?; + + Ok(()) + } +} + +fn get_db_path() -> PathBuf { + if let Ok(env_path) = std::env::var("TINYDEW_DB_PATH") { + return PathBuf::from(env_path); + } + + let base = dirs::data_local_dir().unwrap_or_else(|| PathBuf::from(".")); + base.join("tinydew").join("tinydew.sqlite") +} + +static DATABASE: std::sync::OnceLock> = std::sync::OnceLock::new(); + +fn get_database() -> &'static std::sync::Mutex { + DATABASE.get_or_init(|| { + let path = get_db_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::sync::Mutex::new(GameDatabase::new(&path).expect("Failed to open database")) + }) +} + +pub fn load_game() -> GameState { + get_database().lock().unwrap().load().unwrap_or_default() +} + +pub fn save_game(state: &GameState) { + get_database() + .lock() + .unwrap() + .save(state) + .expect("Failed to save game"); +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..771bc05 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct GameTime { + pub day: u32, + pub minutes: u32, +} + +impl GameTime { + pub fn new(day: u32, hour: u32, minute: u32) -> Self { + Self { + day, + minutes: hour * 60 + minute, + } + } + + pub fn start_of_day(day: u32) -> Self { + Self::new(day, 6, 0) + } + + pub fn hour(&self) -> u32 { + self.minutes / 60 + } + + pub fn minute(&self) -> u32 { + self.minutes % 60 + } + + pub fn format_time(&self) -> String { + format!("{:02}:{:02}", self.hour(), self.minute()) + } + + pub fn is_night(&self) -> bool { + self.hour() >= 20 || self.hour() < 6 + } + + pub fn advance(&mut self, minutes: u32) { + self.minutes += minutes; + while self.minutes >= 24 * 60 { + self.minutes -= 24 * 60; + self.day += 1; + } + } + + pub fn next_day(&mut self) { + self.day += 1; + self.minutes = 6 * 60; + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..c4c99c4 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,178 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Region { + Farm, + EastPath, + Square, + SouthRiver, +} + +impl std::fmt::Display for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Region::Farm => write!(f, "Farm"), + Region::EastPath => write!(f, "EastPath"), + Region::Square => write!(f, "Square"), + Region::SouthRiver => write!(f, "SouthRiver"), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + +impl Direction { + pub fn delta(&self) -> (i32, i32) { + match self { + Direction::Up => (0, -1), + Direction::Down => (0, 1), + Direction::Left => (-1, 0), + Direction::Right => (1, 0), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TileType { + Boundary, + Grass, + Soil, + Crop(CropType, bool), + House, + PathEast, + PathFarm, + PathSquare, + PathSouthRiver, + PathSouthRiverGate, + Mushroom, + Fountain, + River, + RiverBubble, + Wonder, + Flower(FlowerState), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct FlowerState { + pub mature: bool, +} + +impl TileType { + pub fn is_walkable(&self) -> bool { + match self { + TileType::Boundary + | TileType::House + | TileType::Mushroom + | TileType::Fountain + | TileType::River + | TileType::RiverBubble + | TileType::Wonder => false, + TileType::Crop(_, mature) => !(*mature), + TileType::Flower(state) => !state.mature, + _ => true, + } + } + + pub fn emoji(&self) -> &'static str { + match self { + TileType::Boundary => "🌳", + TileType::Grass => "🌿", + TileType::Soil => "🍃", + TileType::Crop(crop, mature) => { + if *mature { + crop.produce_emoji() + } else { + "🌱" + } + } + TileType::House => "🏠", + TileType::PathEast + | TileType::PathFarm + | TileType::PathSquare + | TileType::PathSouthRiver + | TileType::PathSouthRiverGate => "🌿", + TileType::Mushroom => "🍄", + TileType::Fountain => "⛲", + TileType::River => "🌊", + TileType::RiverBubble => "🫧", + TileType::Wonder => "🦋", + TileType::Flower(state) => { + if state.mature { + "🌺" + } else { + "🌱" + } + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CropType { + Carrot, + Strawberry, + Cauliflower, + Flower, +} + +impl CropType { + pub fn maturity_days(&self) -> u32 { + match self { + CropType::Carrot => 3, + CropType::Strawberry => 4, + CropType::Cauliflower => 5, + CropType::Flower => 2, + } + } + + pub fn produce_emoji(&self) -> &'static str { + match self { + CropType::Carrot => "🥕", + CropType::Strawberry => "🍓", + CropType::Cauliflower => "🥦", + CropType::Flower => "🌺", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Weather { + Sunny, + Cloudy, + Rainy, +} + +impl Weather { + pub fn emoji(&self, is_night: bool) -> &'static str { + if is_night { + "🌙" + } else { + match self { + Weather::Sunny => "☀️", + Weather::Cloudy => "⛅", + Weather::Rainy => "🌧", + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FishType { + Common, + Rare, +} + +impl FishType { + pub fn emoji(&self) -> &'static str { + match self { + FishType::Common => "🐟", + FishType::Rare => "🐠", + } + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..4e2666b --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,73 @@ +use crate::state::GameState; + +pub fn render_status(state: &GameState) -> String { + let mut output = String::new(); + + let weather_emoji = state.weather.emoji(state.time.is_night()); + output.push_str(&format!( + "tinydew day {} {} {}\n", + state.time.day, + weather_emoji, + state.time.format_time() + )); + + output.push('\n'); + + if let Some(map) = state.get_current_map() { + for y in 0..map.height { + for x in 0..map.width { + if state.player.x == x && state.player.y == y { + output.push('🧑'); + } else { + let tile = + if let Some(crop_data) = state.crops.get(&(state.player.region, x, y)) { + crate::types::TileType::Crop(crop_data.crop_type, crop_data.is_mature()) + } else { + map.get(x, y).unwrap_or(crate::types::TileType::Boundary) + }; + output.push_str(tile.emoji()); + } + } + output.push('\n'); + } + } + + output.push('\n'); + + if state.inventory.seeds > 0 { + output.push_str(&format!("seeds: 🫙 x{}\n", state.inventory.seeds)); + } + + if !state.inventory.produce.is_empty() { + for (emoji, count) in &state.inventory.produce { + output.push_str(&format!("{} x{}\n", emoji, count)); + } + } + + if !state.inventory.forage.is_empty() { + for (emoji, count) in &state.inventory.forage { + output.push_str(&format!("{} x{}\n", emoji, count)); + } + } + + if !state.inventory.fish.is_empty() { + for (emoji, count) in &state.inventory.fish { + output.push_str(&format!("{} x{}\n", emoji, count)); + } + } + + if state.inventory.seeds > 0 + || !state.inventory.produce.is_empty() + || !state.inventory.forage.is_empty() + || !state.inventory.fish.is_empty() + { + output.push('\n'); + } + + output.push_str(&format!("Money: 💰 ${}\n", state.money)); + output.push('\n'); + + output.push_str(&state.bottom_message); + + output +} diff --git a/src/weather.rs b/src/weather.rs new file mode 100644 index 0000000..e4c3ba8 --- /dev/null +++ b/src/weather.rs @@ -0,0 +1,23 @@ +use crate::types::Weather; + +pub fn roll_weather(day: u32, seed: u64) -> Weather { + if day == 1 { + return Weather::Sunny; + } + + let festival_day = 28; + if day == festival_day { + return Weather::Sunny; + } + + let hash = seed.wrapping_mul(1103515245).wrapping_add(12345); + let value = (hash % 100) as u32; + + if value < 50 { + Weather::Sunny + } else if value < 80 { + Weather::Cloudy + } else { + Weather::Rainy + } +}