Compare commits
68 commits
4fecab5428
...
5ae8c7dc25
Author | SHA1 | Date | |
---|---|---|---|
yuni | 5ae8c7dc25 | ||
yuni | 982ea00fc7 | ||
yuni | ef319766fc | ||
yuni | a01bf79542 | ||
yuni | 8ce084b72f | ||
yuni | cd98b16926 | ||
yuni | 02499e8d05 | ||
yuni | c273217f65 | ||
yuni | 4e8794338a | ||
yuni | 8c7a856717 | ||
yuni | f1512e01c9 | ||
yuni | eedc379c8d | ||
yuni | 2eb68e94f7 | ||
yuni | 43756fc09c | ||
yuni | 0f0d1aa1a9 | ||
yuni | 4ed006c548 | ||
yuni | e579fdcdb7 | ||
yuni | 4fa486946e | ||
yuni | e6c9ee9f3f | ||
yuni | 5082449c11 | ||
yuni | a3c87e1651 | ||
yuni | f8913b8fa3 | ||
yuni | 94ea7ecec2 | ||
yuni | 727cdcb0c1 | ||
yuni | 009a7ba1dd | ||
yuni | 9e121cf633 | ||
yuni | 0c622f28ab | ||
yuni | 625bf21c84 | ||
yuni | 3b540b290d | ||
yuni | 8636b08b5f | ||
yuni | 7ec52b3503 | ||
yuni | 83f3f03aff | ||
yuni | 9d54a9d412 | ||
yuni | f2246a247f | ||
yuni | a3661cc43f | ||
yuni | 2c1dacbf03 | ||
yuni | 0047c4eda4 | ||
yuni | 28cb1c09fd | ||
yuni | beaf8cff47 | ||
yuni | ee818beea4 | ||
yuni | 8c97a962b2 | ||
yuni | 1a94c31d62 | ||
yuni | 6275a64d7c | ||
yuni | 91bf2ddc54 | ||
yuni | 79351dc4d0 | ||
yuni | 4dd195e17a | ||
yuni | 3d26b0915d | ||
yuni | 46a030f15e | ||
yuni | d04b400fad | ||
yuni | 77c1bd1e6a | ||
yuni | 5817944a79 | ||
yuni | cd13b529c3 | ||
yuni | 76272a7fc2 | ||
yuni | d9af542d54 | ||
yuni | e16a650b22 | ||
yuni | 7be6b0746f | ||
yuni | 974bf9cb8d | ||
yuni | e56f931951 | ||
yuni | 24a9b208bd | ||
yuni | 1614ece72a | ||
yuni | 169b9ee257 | ||
yuni | c6750eae46 | ||
yuni | 159dfe8e19 | ||
yuni | 797b106255 | ||
yuni | 2f82a27ab2 | ||
yuni | d3fb7422bf | ||
yuni | 1433773784 | ||
yuni | 727d28089f |
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,3 +1,18 @@
|
|||
# Git Development Version
|
||||
|
||||
- Implement gravity and orbiting (everything's super fast now)
|
||||
- Implement radio stations
|
||||
- Implement loading whole scenes from blender files
|
||||
- Implement saving/loading settings from configuration file
|
||||
- Starting point also orbits in real time. You may start in an eclipse now.
|
||||
- Space key & Speedometer now work relative to orbital velocity
|
||||
- Add emergency conversation options when low on oxygen
|
||||
- Add suffocation sound effects
|
||||
- Add new character "Sus" near the pizzeria
|
||||
- More conversations
|
||||
- Regression: Broken generic asteroids -> disabled for now
|
||||
- Regression: Broken collision on light orbs, supply crates
|
||||
|
||||
# v0.9.2
|
||||
|
||||
- Implement customizable player avatars
|
||||
|
|
68
Cargo.lock
generated
68
Cargo.lock
generated
|
@ -1097,6 +1097,16 @@ dependencies = [
|
|||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blend"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56036f5e6c7ce6edb901e7a75ec34d6da2472ad3a2cbcd00df60a518b071a5b2"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
|
@ -1558,6 +1568,27 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[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 = "dispatch"
|
||||
version = "0.2.0"
|
||||
|
@ -2296,6 +2327,22 @@ dependencies = [
|
|||
"redox_syscall 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.13"
|
||||
|
@ -2756,13 +2803,19 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"libredox 0.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2772,6 +2825,8 @@ dependencies = [
|
|||
"bevy",
|
||||
"bevy_embedded_assets",
|
||||
"bevy_xpbd_3d",
|
||||
"blend",
|
||||
"dirs",
|
||||
"embed-resource",
|
||||
"fastrand",
|
||||
"regex",
|
||||
|
@ -3065,6 +3120,17 @@ dependencies = [
|
|||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox 0.1.3",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
|
|
|
@ -31,6 +31,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
serde_yaml = "0.9"
|
||||
|
||||
# For reading/writing the player's configuration file.
|
||||
dirs = "5.0"
|
||||
toml_edit = { version = "0.22", features = ["serde"] }
|
||||
|
||||
[dependencies.bevy]
|
||||
|
@ -64,6 +65,7 @@ features = ["3d", "f64", "parry-f64", "parallel", "async-collider"]
|
|||
# a [target[...]build-dependencies] block because in case of cross-compiling, the
|
||||
# build.rs will be compiled for a different, non-windows target than the main executable.
|
||||
embed-resource = "1.6.3" # embedding of .exe metadata
|
||||
blend = "0.8.0"
|
||||
|
||||
[features]
|
||||
default = ["x11", "embed_assets"]
|
||||
|
|
16
README.md
16
README.md
|
@ -1,6 +1,6 @@
|
|||
![OutFly Screenshot](doc/branding/banner.jpg)
|
||||
|
||||
[Features](#features) • [Controls](#controls) • [Running OutFly](#running-outfly) • [Troubleshooting](#troubleshooting)
|
||||
[Features](#features) • [Tutorial](#tutorial) • [Controls](#controls) • [Running OutFly](#running-outfly) • [Troubleshooting](#troubleshooting)
|
||||
|
||||
# OutFly
|
||||
|
||||
|
@ -27,6 +27,20 @@ Source code: https://codeberg.org/outfly/outfly
|
|||
- Written in [Rust](https://www.rust-lang.org) with the [Bevy game engine](https://bevyengine.org)
|
||||
- Status: Early access, not much content
|
||||
|
||||
# Tutorial
|
||||
|
||||
OutFly has typical game controls like mouse movement, `A/W/S/D` for horizontal movement, `Ctrl/Shift` for vertical movement. But in space, there is no friction to slow you down after the movement. [You just keep on moving](https://en.wikipedia.org/wiki/Newton%27s_laws_of_motion#First_law), until you hit something or accelerate in the opposite direction.
|
||||
|
||||
The `SPACE` key helps you move more intuitively by slowing you down. Hold it for a while to come to a halt. Combine it with movement keys for slower, more precise movement. You can also click to select another object and then hold the `SPACE` key to **match your velocity** to the target object. Essential for spaceflight!
|
||||
|
||||
When you're ready, take a look around, explore the starting area. There is a friendly person floating nearby in a red space suit that would love to talk to you.
|
||||
|
||||
The game is dark. After all, space is dark. The planets and moons orbit in real time, the map changes depending on when you play, and if you're unlucky, the sun may be eclipsed and everything's even darker! Try the flashlight (`f` key), turning off shadows in the menu, and make sure that Augmented Reality is on (`TAB` key) which gives you a little extra light amplification.
|
||||
|
||||
Press `Esc` for the menu to restart the game if you get lost, explore more key bindings, game features, and the **achievements** to get some guidance on what you can do in the game.
|
||||
|
||||
But in the end, OutFly is an open world game with no true goals other than the ones you set for yourself. Just lean back, get cozy, and drift towards whatever catches your eye :)
|
||||
|
||||
# Controls
|
||||
|
||||
Press **ESC** to view these any time from the in-game menu.
|
||||
|
|
BIN
assets/models/sus.glb
Normal file
BIN
assets/models/sus.glb
Normal file
Binary file not shown.
BIN
assets/sounds/gasp.ogg
Normal file
BIN
assets/sounds/gasp.ogg
Normal file
Binary file not shown.
BIN
assets/sounds/gasprelief.ogg
Normal file
BIN
assets/sounds/gasprelief.ogg
Normal file
Binary file not shown.
46
build.rs
46
build.rs
|
@ -8,10 +8,54 @@
|
|||
// + + + ███
|
||||
// + ▀████████████████████████████████████████████████████▀
|
||||
|
||||
fn main() {
|
||||
use blend::Blend;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let target = std::env::var("TARGET").unwrap();
|
||||
if target.contains("windows") {
|
||||
println!("cargo:warning=Embedding Windows Icon");
|
||||
embed_resource::compile("build/windows/icon.rc");
|
||||
}
|
||||
|
||||
let file = File::create("src/data/scenes.in");
|
||||
if let Ok(mut file) = file {
|
||||
write!(&file, "[\n")?;
|
||||
extract_scene(&mut file, "test", "src/blender/scene_test.blend")?;
|
||||
write!(&file, "]\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_scene(file: &mut File, scene_name: &str, blend_file: &str) -> std::io::Result<()> {
|
||||
let blend = Blend::from_path(blend_file).expect("error loading blend file");
|
||||
for obj in blend.instances_with_code(*b"OB") {
|
||||
let loc: Vec<f32> = if obj.is_valid("loc") {
|
||||
obj.get_f32_vec("loc")
|
||||
} else {
|
||||
vec![0.0, 0.0, 0.0]
|
||||
};
|
||||
let rot: Vec<f32> = if obj.is_valid("rot") {
|
||||
obj.get_f32_vec("rot")
|
||||
} else {
|
||||
vec![0.0, 0.0, 0.0]
|
||||
};
|
||||
let name = obj.get("id").get_string("name");
|
||||
if let Some(name) = get_scene_object_name(name.as_str()) {
|
||||
write!(file, "({scene_name:?}, {name:?}, {loc:?}, {rot:?}),\n")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_scene_object_name(full_id: &str) -> Option<&str> {
|
||||
let prefix = "OBLOAD=";
|
||||
if full_id.starts_with(prefix) {
|
||||
let remainder: &str = &full_id[prefix.len()..];
|
||||
let parts: Vec<&str> = remainder.split('.').collect();
|
||||
let name = parts[0];
|
||||
return Some(name);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
+ ▀████████████████████████████████████████████████████▀
|
||||
```
|
||||
|
||||
# OutFly Roadmap
|
||||
|
||||
This file is a place for planning and documenting the game design.
|
||||
|
||||
# Why I made this game
|
||||
|
@ -179,16 +177,105 @@ Items:
|
|||
- Fuel canister
|
||||
- UltraCapacitor
|
||||
|
||||
# People
|
||||
|
||||
There are relatively few people in the game due to Jupiter's rings not being
|
||||
extensively settled. The rings are more of a niche thing for adventurers and
|
||||
visionaries at the time of the game.
|
||||
|
||||
Those few people should have maximally fleshed out conversations though, making
|
||||
the player happy to see a friendly face when they do appear.
|
||||
|
||||
Each character should have an interesting, unique personality that sets them
|
||||
apart from the other characters. Let's outline the personalities/backstories:
|
||||
|
||||
[THIS IS JUST BRAINSTORMING FOR NOW, ONLY PARTLY IMPLEMENTED IN THE GAME]
|
||||
|
||||
1. Icarus @ starting area:
|
||||
- "Tutorial character"
|
||||
- Very chill, nonchalant
|
||||
- Goes out of its way to help you
|
||||
- Touristy, sightseer
|
||||
- Very absorbed by his VR headset
|
||||
- Grew up on a space ship where VR was the best way to see interesting things
|
||||
- Travels the solar system to find beauty
|
||||
- Not interested in creating anything
|
||||
- Rides a Cruiser
|
||||
2. Nox @ pizzeria:
|
||||
- Wholesome, easy-going
|
||||
- Wanted to get away from civilization to have some peace and quiet
|
||||
- Built a pizzeria into an asteroid
|
||||
- Doesn't seem to care that so few people visit his restaurant, just keeps doing his thing
|
||||
- Fascinated with the old times of the 1980s, he called his pizzeria "Old Earth Pizza"
|
||||
- Wants to take care of people and impress people with his hospitality
|
||||
- Reliable, people in the area look up to him as sort of a "governor"/"sheriff"
|
||||
- Biotech/brewery nerd, grows his own algae for the pizza ingredients
|
||||
3. ??? @ pizzeria:
|
||||
- Thrill-seeker, racer
|
||||
- Cheeky, teasing
|
||||
- Frequent guest at the Pizzeria to refuel and rest
|
||||
- Good for some small talk
|
||||
- Nerd about engineering, propulsion, rocket science
|
||||
- Rides a MeteorAceGT
|
||||
4. ??? @ pizzeria or workshop:
|
||||
- Engineer
|
||||
- ADHD
|
||||
- Deploys communications infrastructure across the rings
|
||||
- Just passing through
|
||||
- Grew up on Earth, is way too excited about being in space
|
||||
- Totally a furry
|
||||
- Has big plans for this area, but not enough time to implement everything
|
||||
- Nerd about high energy communications technology
|
||||
5. ??? @ workshop:
|
||||
- Mechanic
|
||||
- Operates a workshop to craft and mod space vehicles
|
||||
- Nerd about space suits and vehicles
|
||||
6. Ash @ hideout:
|
||||
- Monk master of a cult of Buddhist origins
|
||||
- Uptight, self-disciplined, driven, wise
|
||||
- Not very agreeable
|
||||
- Operates a construction site for a meditation retreat in a hollowed-out asteroid
|
||||
- Deep distrust of AIs, wants to construct the hideout by hand
|
||||
- Not interested in small talk, but up for deep philosophical debates
|
||||
7. River @ hideout:
|
||||
- Monk apprentice
|
||||
- Open-minded
|
||||
- Had everything in her life on one of the planets
|
||||
- But ached for profoundness, desperate for a meaning of life
|
||||
- Frustrated by the rigidity of Ash, she thinks she knows better how to run the place
|
||||
8. Rain @ hideout:
|
||||
- Engineer, tinkerer
|
||||
- Very shy, unsocial
|
||||
- Helps the monks to construct the hideout
|
||||
- Totally insecure about her job, since she has no formal education as architect, she's just winging it, but the monks will need a 100% functioning home to survive
|
||||
- IT Security nerd
|
||||
- Operates a synthwave radio station, piggybacking on the communications infrastructure
|
||||
|
||||
Minor characters:
|
||||
|
||||
- Yuni @ secret location:
|
||||
- Easter egg character
|
||||
- Meta, breaking the 3rd wall
|
||||
- 梓涵 @ near starting area:
|
||||
- Is dead from the start. Or can you save her?
|
||||
- Rudy @ serenity bus station:
|
||||
- Is unconscious, frozen, waiting for the bus
|
||||
- Clippy @ various locations:
|
||||
- AIs which could get personalities as well
|
||||
|
||||
# Worldbuilding
|
||||
## People
|
||||
|
||||
- Icarus
|
||||
- 梓涵
|
||||
- Nox
|
||||
- Rudy
|
||||
- Yuni
|
||||
- Pizzeria Clippy
|
||||
- Bus Station Clippys
|
||||
- Icarus [it]
|
||||
- 梓涵 [she]
|
||||
- Nox [he]
|
||||
- Rudy [he]
|
||||
- Yuni [no pronoun]
|
||||
- Ash [they]
|
||||
- River [she]
|
||||
- Rain [she]
|
||||
- Pizzeria Clippy [it]
|
||||
- Bus Station Clippys [it]
|
||||
|
||||
## Other life forms
|
||||
|
86
src/actor.rs
86
src/actor.rs
|
@ -30,8 +30,9 @@ impl Plugin for ActorPlugin {
|
|||
(
|
||||
update_physics_lifeforms,
|
||||
update_power,
|
||||
handle_gravity,
|
||||
handle_wants_maxrotation,
|
||||
handle_wants_maxvelocity,
|
||||
handle_wants_maxvelocity.run_if(any_with_component::<WantsMaxVelocity>),
|
||||
handle_wants_lookat.run_if(alive),
|
||||
),
|
||||
);
|
||||
|
@ -118,6 +119,7 @@ pub struct ExperiencesGForce {
|
|||
pub visual_effect_threshold: f32,
|
||||
pub visual_effect: f32,
|
||||
pub last_linear_velocity: DVec3,
|
||||
pub gravitational_component: DVec3,
|
||||
pub ignore_gforce_seconds: f32,
|
||||
}
|
||||
impl Default for ExperiencesGForce {
|
||||
|
@ -127,8 +129,9 @@ impl Default for ExperiencesGForce {
|
|||
damage_threshold: 100.0,
|
||||
visual_effect_threshold: 20.0,
|
||||
visual_effect: 0.0,
|
||||
last_linear_velocity: DVec3::splat(0.0),
|
||||
ignore_gforce_seconds: 0.0,
|
||||
last_linear_velocity: DVec3::ZERO,
|
||||
gravitational_component: DVec3::ZERO,
|
||||
ignore_gforce_seconds: 0.01,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +159,11 @@ pub struct WantsMaxVelocity(pub f64);
|
|||
#[derive(Component)]
|
||||
pub struct WantsToLookAt(pub String);
|
||||
#[derive(Component)]
|
||||
pub struct WantsMatchVelocityWith(pub String);
|
||||
#[derive(Component)]
|
||||
pub struct Identifier(pub String);
|
||||
#[derive(Component)]
|
||||
pub struct OrbitsJupiter;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct LifeForm {
|
||||
|
@ -535,25 +542,53 @@ fn handle_wants_maxrotation(
|
|||
}
|
||||
}
|
||||
|
||||
/// Slows down NPC's movement until they reach their target velocity.
|
||||
fn handle_wants_maxvelocity(
|
||||
time: Res<Time>,
|
||||
mut query: Query<(&mut LinearVelocity, &Engine, &WantsMaxVelocity)>,
|
||||
mut query: Query<(
|
||||
&Position,
|
||||
&mut LinearVelocity,
|
||||
&Engine,
|
||||
&WantsMaxVelocity,
|
||||
Option<&OrbitsJupiter>,
|
||||
Option<&WantsMatchVelocityWith>,
|
||||
)>,
|
||||
id2v: Res<game::Id2V>,
|
||||
jupiter_pos: Res<game::JupiterPos>,
|
||||
) {
|
||||
let dt = time.delta_seconds();
|
||||
for (mut v, engine, maxv) in &mut query {
|
||||
let total = v.0.length();
|
||||
if total <= maxv.0 + EPSILON {
|
||||
if total > maxv.0 {
|
||||
v.0 = DVec3::splat(0.0);
|
||||
for (pos, mut v, engine, maxv, orbits_jupiter, matchwith) in &mut query {
|
||||
let target_velocity = if let Some(matchwith) = matchwith {
|
||||
if let Some(target_v) = id2v.0.get(&matchwith.0) {
|
||||
*target_v
|
||||
} else {
|
||||
warn!("Can't match velocity with nonexisting ID {}", matchwith.0);
|
||||
continue;
|
||||
}
|
||||
} else if orbits_jupiter.is_some() {
|
||||
let relative_pos = pos.0 - jupiter_pos.0;
|
||||
nature::orbital_velocity(relative_pos, nature::JUPITER_MASS)
|
||||
} else {
|
||||
DVec3::ZERO
|
||||
};
|
||||
let relative_velocity = v.0 - target_velocity;
|
||||
let relative_speed = relative_velocity.length();
|
||||
|
||||
if relative_speed <= maxv.0 + EPSILON {
|
||||
// it's already pretty close to the target
|
||||
if relative_speed > maxv.0 {
|
||||
// but not quite the target, so let's set it to the target
|
||||
v.0 = target_velocity;
|
||||
}
|
||||
} else {
|
||||
// slow it down a little bit
|
||||
// TODO: respect engine parameters for different thrusts for different directions
|
||||
let avg_thrust =
|
||||
(engine.thrust_forward + engine.thrust_back + engine.thrust_sideways) / 3.0;
|
||||
let acceleration = (avg_thrust * dt) as f64 * -v.0;
|
||||
let acceleration = (avg_thrust * dt) as f64 * -relative_velocity;
|
||||
v.0 += acceleration;
|
||||
if v.0.length() + EPSILON < acceleration.length() {
|
||||
v.0 = DVec3::splat(0.0);
|
||||
v.0 = target_velocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -610,8 +645,10 @@ fn handle_gforce(
|
|||
let dt = time.delta_seconds();
|
||||
let factor = 1.0 / dt / nature::EARTH_GRAVITY;
|
||||
for (v, mut hp, mut gforce) in &mut q_actor {
|
||||
gforce.gforce = factor * (v.0 - gforce.last_linear_velocity).length() as f32;
|
||||
gforce.gforce = factor
|
||||
* (v.0 - gforce.last_linear_velocity - gforce.gravitational_component).length() as f32;
|
||||
gforce.last_linear_velocity = v.0;
|
||||
gforce.gravitational_component = DVec3::ZERO;
|
||||
if gforce.ignore_gforce_seconds > 0.0 {
|
||||
gforce.ignore_gforce_seconds -= dt;
|
||||
continue;
|
||||
|
@ -632,3 +669,28 @@ fn handle_gforce(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_gravity(
|
||||
time: Res<Time>,
|
||||
mut q_pos: Query<
|
||||
(
|
||||
&Position,
|
||||
&mut LinearVelocity,
|
||||
Option<&mut ExperiencesGForce>,
|
||||
),
|
||||
With<OrbitsJupiter>,
|
||||
>,
|
||||
jupiter_pos: Res<game::JupiterPos>,
|
||||
) {
|
||||
let dt = time.delta_seconds() as f64;
|
||||
|
||||
// this assumes prograde orbits for every object
|
||||
for (pos, mut v, gforce_maybe) in &mut q_pos {
|
||||
let relative_pos = pos.0 - jupiter_pos.0;
|
||||
let accel = dt * nature::gravitational_acceleration(relative_pos, nature::JUPITER_MASS);
|
||||
if let Some(mut gforce) = gforce_maybe {
|
||||
gforce.gravitational_component += accel;
|
||||
}
|
||||
v.0 += accel;
|
||||
}
|
||||
}
|
||||
|
|
69
src/audio.rs
69
src/audio.rs
|
@ -23,6 +23,7 @@ impl Plugin for AudioPlugin {
|
|||
Update,
|
||||
(
|
||||
play_zoom_sfx,
|
||||
play_gasp_sfx,
|
||||
respawn_sinks.run_if(on_event::<RespawnSinksEvent>()),
|
||||
pause_all.run_if(on_event::<PauseAllSfxEvent>()),
|
||||
),
|
||||
|
@ -47,12 +48,12 @@ pub struct ZoomTimer(Timer);
|
|||
|
||||
const PATHS: &[(SfxType, Sfx, &str)] = &[
|
||||
(
|
||||
SfxType::BGM,
|
||||
SfxType::Radio,
|
||||
Sfx::BGM,
|
||||
"music/Aleksey Chistilin - Cinematic Cello.ogg",
|
||||
),
|
||||
(
|
||||
SfxType::BGMNoAR,
|
||||
SfxType::Radio,
|
||||
Sfx::BGMActualJupiterRecording,
|
||||
"music/JupiterRecording.ogg",
|
||||
),
|
||||
|
@ -64,6 +65,8 @@ const PATHS: &[(SfxType, Sfx, &str)] = &[
|
|||
(SfxType::LoopSfx, Sfx::Ion, "sounds/ion.ogg"),
|
||||
(SfxType::LoopSfx, Sfx::Rocket, "sounds/rocket.ogg"),
|
||||
(SfxType::LoopSfx, Sfx::Thruster, "sounds/thruster.ogg"),
|
||||
(SfxType::LoopSfx, Sfx::Gasp, "sounds/gasp.ogg"),
|
||||
(SfxType::OneOff, Sfx::GaspRelief, "sounds/gasprelief.ogg"),
|
||||
(SfxType::OneOff, Sfx::Achieve, "sounds/achieve.ogg"),
|
||||
(
|
||||
SfxType::OneOff,
|
||||
|
@ -99,6 +102,8 @@ pub enum Sfx {
|
|||
Crash,
|
||||
ElectricMotor,
|
||||
EnterVehicle,
|
||||
Gasp,
|
||||
GaspRelief,
|
||||
IncomingChatMessage,
|
||||
Ion,
|
||||
Ping,
|
||||
|
@ -127,8 +132,7 @@ pub fn str2sfx(sfx_label: &str) -> Sfx {
|
|||
}
|
||||
|
||||
pub enum SfxType {
|
||||
BGM,
|
||||
BGMNoAR,
|
||||
Radio,
|
||||
LoopSfx,
|
||||
OneOff,
|
||||
}
|
||||
|
@ -161,8 +165,8 @@ pub fn setup(
|
|||
pub fn respawn_sinks(
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
settings: Res<var::Settings>,
|
||||
q_audiosinks: Query<Entity, (With<AudioSink>, With<Sfx>)>,
|
||||
settings: Res<Settings>,
|
||||
) {
|
||||
for sink in &q_audiosinks {
|
||||
commands.entity(sink).despawn();
|
||||
|
@ -170,27 +174,14 @@ pub fn respawn_sinks(
|
|||
for (sfxtype, sfx, path) in PATHS {
|
||||
let source = asset_server.load(*path);
|
||||
match sfxtype {
|
||||
SfxType::BGM => {
|
||||
SfxType::Radio => {
|
||||
commands.spawn((
|
||||
*sfx,
|
||||
AudioBundle {
|
||||
source,
|
||||
settings: PlaybackSettings {
|
||||
mode: PlaybackMode::Loop,
|
||||
paused: settings.mute_music || !settings.hud_active,
|
||||
..default()
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
SfxType::BGMNoAR => {
|
||||
commands.spawn((
|
||||
*sfx,
|
||||
AudioBundle {
|
||||
source,
|
||||
settings: PlaybackSettings {
|
||||
mode: PlaybackMode::Loop,
|
||||
paused: settings.mute_music || settings.hud_active,
|
||||
paused: !settings.is_radio_playing(*sfx).unwrap_or(true),
|
||||
..default()
|
||||
},
|
||||
},
|
||||
|
@ -236,17 +227,12 @@ pub fn play_sfx(
|
|||
|
||||
pub fn toggle_music(q_audiosinks: Query<(&AudioSink, &Sfx)>, settings: Res<var::Settings>) {
|
||||
for (bgm_sink, sfx) in &q_audiosinks {
|
||||
let play = match *sfx {
|
||||
Sfx::BGM => settings.hud_active,
|
||||
Sfx::BGMActualJupiterRecording => !settings.hud_active,
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if settings.mute_music || !play {
|
||||
bgm_sink.pause();
|
||||
} else {
|
||||
if let Some(play) = settings.is_radio_playing(*sfx) {
|
||||
if play {
|
||||
bgm_sink.play();
|
||||
} else {
|
||||
bgm_sink.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -272,6 +258,29 @@ pub fn play_zoom_sfx(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn play_gasp_sfx(
|
||||
player: Query<&actor::Suit, With<actor::Player>>,
|
||||
mut ew_sfx: EventWriter<PlaySfxEvent>,
|
||||
q_audiosinks: Query<(&audio::Sfx, &AudioSink)>,
|
||||
) {
|
||||
if let Ok(suit) = player.get_single() {
|
||||
for (sfxtype, sink) in &q_audiosinks {
|
||||
if *sfxtype != Sfx::Gasp {
|
||||
continue;
|
||||
}
|
||||
if suit.oxygen <= 0.0 {
|
||||
sink.set_volume(0.6);
|
||||
sink.play();
|
||||
} else {
|
||||
if !sink.is_paused() {
|
||||
ew_sfx.send(PlaySfxEvent(Sfx::GaspRelief));
|
||||
sink.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause_all(q_audiosinks: Query<&AudioSink, With<Sfx>>) {
|
||||
for sink in &q_audiosinks {
|
||||
sink.pause();
|
||||
|
|
BIN
src/blender/scene_test.blend
Normal file
BIN
src/blender/scene_test.blend
Normal file
Binary file not shown.
BIN
src/blender/sus.blend
Normal file
BIN
src/blender/sus.blend
Normal file
Binary file not shown.
|
@ -416,6 +416,7 @@ pub fn apply_input_to_player(
|
|||
time: Res<Time>,
|
||||
mut commands: Commands,
|
||||
settings: Res<var::Settings>,
|
||||
jupiter_pos: Res<game::JupiterPos>,
|
||||
windows: Query<&Window, With<PrimaryWindow>>,
|
||||
mut mouse_events: EventReader<MouseMotion>,
|
||||
key_input: Res<ButtonInput<KeyCode>>,
|
||||
|
@ -426,6 +427,7 @@ pub fn apply_input_to_player(
|
|||
Entity,
|
||||
&Transform,
|
||||
&mut actor::Engine,
|
||||
&Position,
|
||||
&mut LinearVelocity,
|
||||
&mut ExternalTorque,
|
||||
Option<&actor::PlayerDrivesThis>,
|
||||
|
@ -451,15 +453,15 @@ pub fn apply_input_to_player(
|
|||
win_res_y = 1050.0;
|
||||
}
|
||||
|
||||
if let Ok((player_entity, player_transform, mut engine, pos, mut v, mut torque, bike)) =
|
||||
q_playercam.get_single_mut()
|
||||
{
|
||||
let target_v: DVec3 = if let Ok(target) = q_target.get_single() {
|
||||
target.0
|
||||
} else {
|
||||
DVec3::splat(0.0)
|
||||
let relative_pos = pos.0 - jupiter_pos.0;
|
||||
nature::orbital_velocity(relative_pos, nature::JUPITER_MASS)
|
||||
};
|
||||
|
||||
if let Ok((player_entity, player_transform, mut engine, mut v, mut torque, bike)) =
|
||||
q_playercam.get_single_mut()
|
||||
{
|
||||
// Handle key input
|
||||
if focused {
|
||||
if key_input.pressed(settings.key_forward) || settings.cruise_control_active {
|
||||
|
|
60
src/chat.rs
60
src/chat.rs
|
@ -79,6 +79,7 @@ impl Plugin for ChatPlugin {
|
|||
handle_new_conversations.before(handle_chat_events),
|
||||
handle_chat_events.before(handle_chat_scripts),
|
||||
handle_chat_scripts,
|
||||
update_chat_variables,
|
||||
),
|
||||
);
|
||||
app.add_event::<StartConversationEvent>();
|
||||
|
@ -201,31 +202,37 @@ impl ChatDB {
|
|||
sequence: &mut Value,
|
||||
include_db: &HashMap<String, Vec<Value>>,
|
||||
) {
|
||||
let mut changes: Vec<(usize, String)> = Vec::new();
|
||||
if let Some(vector) = sequence.as_sequence_mut() {
|
||||
for (index, item) in vector.iter_mut().enumerate() {
|
||||
let mut index = 0;
|
||||
loop {
|
||||
// this loop seems unnecessarily convoluted, but I had to write it
|
||||
// this way to make rust's borrow checker happy.
|
||||
let item_maybe = vector.get(index);
|
||||
let item = if let Some(item) = item_maybe {
|
||||
item.clone()
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
match item {
|
||||
Value::Mapping(map) => {
|
||||
for (key, value) in map.iter_mut() {
|
||||
Value::Mapping(mut map) => {
|
||||
for (key, value) in map.clone().iter() {
|
||||
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
|
||||
let label = value.to_string();
|
||||
if key == TOKEN_INCLUDE {
|
||||
changes.push((index, value.to_string()));
|
||||
vector.remove(index);
|
||||
if let Some(chat) = include_db.get(&label) {
|
||||
vector.splice(index..index, chat.iter().cloned());
|
||||
}
|
||||
}
|
||||
} else if value.is_sequence() {
|
||||
let value = map.get_mut(key).unwrap();
|
||||
ChatDB::preprocess_includes_recursively(value, include_db);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for (index, label) in changes {
|
||||
if index < vector.len() {
|
||||
vector.remove(index);
|
||||
if let Some(chat) = include_db.get(&label) {
|
||||
vector.splice(index..index, chat.iter().cloned());
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -827,6 +834,7 @@ pub fn handle_chat_scripts(
|
|||
mut ew_effect: EventWriter<visual::SpawnEffectEvent>,
|
||||
mut ew_achievement: EventWriter<game::AchievementEvent>,
|
||||
id2pos: Res<game::Id2Pos>,
|
||||
id2v: Res<game::Id2V>,
|
||||
) {
|
||||
for script in er_chatscript.read() {
|
||||
// Parse the script string
|
||||
|
@ -892,15 +900,22 @@ pub fn handle_chat_scripts(
|
|||
_ => None,
|
||||
};
|
||||
if let Some(station) = busstop {
|
||||
if let Some(target) = id2pos.0.get(&station.to_string()) {
|
||||
pos.0 = *target + DVec3::new(0.0, -1000.0, 0.0);
|
||||
v.0 = DVec3::ZERO;
|
||||
if let Some(target_pos) = id2pos.0.get(&station.to_string()) {
|
||||
pos.0 = *target_pos + DVec3::new(0.0, -1000.0, 0.0);
|
||||
} else {
|
||||
error!(
|
||||
"Could not determine position of actor with ID: '{}'",
|
||||
station
|
||||
);
|
||||
}
|
||||
if let Some(target_v) = id2v.0.get(&station.to_string()) {
|
||||
v.0 = *target_v;
|
||||
} else {
|
||||
error!(
|
||||
"Could not determine velocity of actor with ID: '{}'",
|
||||
station
|
||||
);
|
||||
}
|
||||
} else {
|
||||
error!("Invalid destination for cryotrip chat script: '{}'", param1);
|
||||
}
|
||||
|
@ -931,3 +946,16 @@ pub fn handle_chat_scripts(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_chat_variables(
|
||||
mut vars: ResMut<var::GameVars>,
|
||||
q_player: Query<&actor::Suit, With<actor::Player>>,
|
||||
) {
|
||||
if let Ok(suit) = q_player.get_single() {
|
||||
vars.set_in_scope(
|
||||
"$",
|
||||
"player_oxygen_seconds",
|
||||
(suit.oxygen / nature::OXY_S).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
- set: $met
|
||||
- I found you drifting out cold, and thought, I better watch over you.
|
||||
- Took us here behind that moonlet, to shield you from the micros.
|
||||
- if: "$$player_oxygen_seconds <= 0"
|
||||
HELP! I'M SUFFOCATING:
|
||||
- goto: suffocating
|
||||
- Thank you!:
|
||||
- label: thx
|
||||
- No worries. Folks are stretched thin around this corner, we gotta watch out for each other.
|
||||
|
@ -39,6 +42,9 @@
|
|||
|
||||
- How are you feeling?
|
||||
- label: howru
|
||||
- if: "$$player_oxygen_seconds <= 0"
|
||||
HELP! I'M SUFFOCATING:
|
||||
- goto: suffocating
|
||||
- I feel quite cozy, this space suit feels like a second skin.:
|
||||
- set: friends
|
||||
- Hah, it does, doesn't it?
|
||||
|
@ -64,10 +70,37 @@
|
|||
- goto: EXIT
|
||||
|
||||
|
||||
|
||||
- label: suffocating
|
||||
- if $gaveoxygen:
|
||||
- Again?
|
||||
- I have limited supplies, you know?
|
||||
- But here you go, some more oxygen for you.
|
||||
- if ~$gaveoxygen:
|
||||
- AAAaaaahhhh!
|
||||
- Here, have some of my own oxygen!
|
||||
- script: refilloxygen 0.002
|
||||
- system: Oxygen refilled
|
||||
- set: gaveoxygen
|
||||
- Phew, thank you so much! You saved my life!:
|
||||
- if $friends:
|
||||
- That's what friends are for!
|
||||
- set: friends
|
||||
- I won't always be around though, you gotta learn to look out for yourself.
|
||||
- There are various places here that stockpile oxygen and supplies.
|
||||
- I recommend Old Earth Pizza, just down the orbit.
|
||||
- You can also find oxygen at StarTrans cargo services. Look for the green cross.
|
||||
- Anything else I can do to help?
|
||||
- goto: help
|
||||
|
||||
|
||||
- label: help
|
||||
- if: "$$player_oxygen_seconds <= 0"
|
||||
HELP! I'M SUFFOCATING:
|
||||
- goto: suffocating
|
||||
- Where are we?:
|
||||
- This is space, my friend.
|
||||
- That massive crescent over there, that's Jupiter.
|
||||
- That massive sphere over there, that's Jupiter.
|
||||
- We're about 150,000km away from its surface, on the very outside of it's rings.
|
||||
- This area is called the Thebe gossamer ring.
|
||||
- The moon Thebe is actually pretty close right now, flinging all those micros at us.
|
||||
|
@ -76,8 +109,17 @@
|
|||
- goto: help
|
||||
- Why am I here?:
|
||||
- That's a very philosophical question.
|
||||
- I don't know.
|
||||
- It's probably related to the choices you made in your life so far.
|
||||
- Nobody really knows why we're here.
|
||||
- One theory is that the quantum fluctuations in the early universe solidified into the macroscopic structures, star clusters, solar systems, planets we see today.
|
||||
- Fascinating. Then what happened?: []
|
||||
- Ok, hold on, this is waaay too detailed!:
|
||||
- Hah, sorry. wasn't sure how much you still remember.
|
||||
- goto: whyhereskip
|
||||
- Then, billions of years of evolution through natural selection turned boring organic molecules into hairless monkeys.
|
||||
- Long story short, then came bicycles, feminism, solar panels, ion engines, and the monkeys ventured into space.
|
||||
- They took evolution into their own monkey hands, spliced in genes for radiation resistance and g-force tolerance for a safer life out here.
|
||||
- label: whyhereskip
|
||||
- Why you're here exactly though, I don't know.
|
||||
- goto: help
|
||||
- What should I do?:
|
||||
- Ah, that's the beauty of life.
|
||||
|
@ -90,6 +132,7 @@
|
|||
- It rides like a punch in the face, don't hurt yourself, ok?
|
||||
- You're too kind!:
|
||||
- Ah, don't mention it!
|
||||
- No, thanks.: []
|
||||
- There's also a half-decent pizza restaurant over there, look for the neon sign.
|
||||
- goto: help
|
||||
- Do you have some money for me?:
|
||||
|
@ -131,6 +174,7 @@
|
|||
- Time to loosen up! Find yourself a cozy place to drift.
|
||||
- Do you have a reservation?
|
||||
- label: reservation
|
||||
- include: generic_help_oxygen_entrypoint
|
||||
- if: ~$reservation
|
||||
...Reservation? Is there not enough space for everybody?:
|
||||
- Ah, space there is.
|
||||
|
@ -186,6 +230,7 @@
|
|||
|
||||
- label: eat
|
||||
- set: $eat
|
||||
- include: generic_help_oxygen_entrypoint
|
||||
- What's on the menu?:
|
||||
- set: $knows-menu
|
||||
- Today's special is Suspicious Spacefunghi.
|
||||
|
@ -249,12 +294,12 @@
|
|||
- But of course! I take care of my guests.
|
||||
- script: refilloxygen 1
|
||||
- system: Oxygen refilled
|
||||
- goto: served
|
||||
- goto: anythingelse
|
||||
- Could you patch up my space suit?:
|
||||
- Right on.
|
||||
- script: repairsuit
|
||||
- system: SuitPatch™ SuperGlue™ applied.
|
||||
- goto: served
|
||||
- goto: anythingelse
|
||||
- Got any coffee?:
|
||||
- Your suit should have a coffee dispenser built right into it.
|
||||
- Naturally, it's not as good as my legendary Old Earth Soykaf!
|
||||
|
@ -273,6 +318,11 @@
|
|||
- label: not hungry
|
||||
- Feel free to hang out as long as you like.
|
||||
- goto: EXIT
|
||||
|
||||
- label: anythingelse
|
||||
- Anything else?
|
||||
- goto: eat
|
||||
|
||||
- label: served
|
||||
- Come back any time!
|
||||
- goto: EXIT
|
||||
|
@ -280,7 +330,13 @@
|
|||
- label: generic_questions
|
||||
- include: generic_questions_serenity
|
||||
- See you around!
|
||||
- goto: EXIT
|
||||
|
||||
- include: generic_help_oxygen_handler
|
||||
- Is there anything else I can help you with?
|
||||
- if $reservation:
|
||||
- goto: eat
|
||||
- goto: reservation
|
||||
|
||||
---
|
||||
|
||||
|
@ -315,6 +371,45 @@
|
|||
---
|
||||
|
||||
|
||||
# Here are two helper components for handling the generic "HELP I NEED OXYGEN"
|
||||
# chat option.
|
||||
# Use the first one as a converation option in every major conversation node by
|
||||
# simply including it, for example:
|
||||
#
|
||||
# - Hello Player!
|
||||
# - How are you doing?
|
||||
# - include: generic_help_oxygen_entrypoint
|
||||
# - Hello NPC, I'm good, how are you?:
|
||||
# - ...
|
||||
#
|
||||
# THIS WILL NOT WORK UNLESS YOU ALSO INCLUDE "generic_help_oxygen_handler"
|
||||
# SOMEWHERE!!! (or implement your own handler for the "needoxygen" label)
|
||||
|
||||
- chat: generic_help_oxygen_entrypoint
|
||||
- if: "$$player_oxygen_seconds <= 1000"
|
||||
HELP! I NEED OXYGEN!:
|
||||
- goto: generic_needoxygen
|
||||
---
|
||||
|
||||
# Use the second one at some unreachable point in the conversation (e.g. right
|
||||
# after a "goto"), followed by some instructions that lead the conversation
|
||||
# flow back into the major conversation nodes. Example:
|
||||
#
|
||||
# - See you around!
|
||||
# - goto: EXIT
|
||||
# - include: generic_help_oxygen_handler
|
||||
# - Is there anything else I can help you with?
|
||||
# - goto: help
|
||||
|
||||
- chat: generic_help_oxygen_handler
|
||||
- label: generic_needoxygen
|
||||
- Sure thing, have some!
|
||||
- script: refilloxygen 1
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
- chat: Drifter
|
||||
- system: "Error: No response"
|
||||
- system: No life signs detected
|
||||
|
@ -328,3 +423,144 @@
|
|||
|
||||
- chat: SubduedClippy
|
||||
- At your service!
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
- chat: Sus
|
||||
- if $helmet:
|
||||
- Come on, cave monkey, I can't give you the snack if you don't take off your helmet. [press ESC]
|
||||
- goto: EXIT
|
||||
- Weeeeeeeeeeeeeeeeeeeee!
|
||||
- if $annoyed:
|
||||
- "Oh... *You* again"
|
||||
- if ~$annoyed:
|
||||
- What's up?
|
||||
- if ~1: ["pass"] # workaround for bug that would make "What's up?" not appear
|
||||
|
||||
# Main Node
|
||||
# ===================
|
||||
- label: entrypoint
|
||||
- include: generic_help_oxygen_entrypoint
|
||||
- What are you?:
|
||||
- Huh?
|
||||
- Why do you even want to know?
|
||||
- Out of scientific curiosity:
|
||||
- Ah, a fellow scientist?
|
||||
- if ~$geologist:
|
||||
- I'm out here for science too. A geologist!
|
||||
- set: geologist
|
||||
- But I'm not an object to be studied. 天哪, I'm a person!
|
||||
- Science doesn't supersede personal dignity and respect.
|
||||
- Don't they teach that where you come from?
|
||||
- goto: entrypoint
|
||||
- You look different than everybody else:
|
||||
- And your belly looks bigger than anyone else's.
|
||||
- set: $annoyed
|
||||
- Do I bother you with that fact?
|
||||
- No.
|
||||
- Leave me alone.
|
||||
- goto: EXIT
|
||||
- Those appendages with opposable thumbs! Amazing!:
|
||||
- Haha, a marvel of engineering!
|
||||
- Swift like tentacles, strong as pincers.
|
||||
- Quite liberating.
|
||||
- Didn't have this back on earth. Now I couldn't live without them.
|
||||
- set: earth
|
||||
- goto: entrypoint
|
||||
- How are you even speaking with me?:
|
||||
- if $annoyed:
|
||||
- Bugger off, bigot.
|
||||
- goto: EXIT
|
||||
- You... actually don't know? You're not one of the bigots?
|
||||
- set: pig
|
||||
- You really never seen a talking pig before?
|
||||
- I didn't even know such a thing existed!:
|
||||
- if $explained:
|
||||
- What do you mean? I just explained it to you.
|
||||
- Short-term episodic amnesia? Attention deficit disorder?
|
||||
- Better get yourself a check-up.
|
||||
- "Anyway, here we go again:"
|
||||
- goto: explain
|
||||
- Wow. We've been part of interplanetary civilization for centuries.
|
||||
- How could you miss that? Where did you grow up?
|
||||
- I... don't actually remember.:
|
||||
- Oh no, are you OK?
|
||||
- Better get yourself a check-up.
|
||||
- "But to answer your question:"
|
||||
- goto: explain
|
||||
- Of course I've seen talking pigs. How do talk though?:
|
||||
- label: explain
|
||||
- set: explained
|
||||
- I just think what I want to say and technology takes care of the rest.
|
||||
- I've got some implants that read out my brain signals
|
||||
- and the suit's computer turns that into words.
|
||||
- This tech changed everything. Conversation makes all the difference.
|
||||
- goto: entrypoint
|
||||
- Pork! Yummy, I'm starving!:
|
||||
- goto: pork
|
||||
- What's your story?:
|
||||
- I'm a geologist.
|
||||
- set: geologist
|
||||
- set: earth
|
||||
- set: $annoyed 0
|
||||
- I just loved the mud and dirt on Earth so much, I decided to study it.
|
||||
- An endlessly fascinating subject.
|
||||
- But the old rock, Earth, has been studied to the end and back.
|
||||
- No scientific frontier in geology anymore.
|
||||
- So I decided to venture into space, hoping to discover something novel.
|
||||
- Came for the science, stayed for the floooooating around!
|
||||
- I'm actually flying! Weeeeeeeee! This is so awesome!
|
||||
- goto: entrypoint
|
||||
- if: $earth
|
||||
What do you miss most out here in space?:
|
||||
- set: $annoyed 0
|
||||
- The mud!
|
||||
- The space suit is quite liberating, with the comms and the appendages.
|
||||
- But I miss being nude, wallowing in the mud.
|
||||
- set: mud
|
||||
- So much dust and rocks out here and I can't touch any of it.
|
||||
- It's driving me nuts!!! Aaaah!!
|
||||
- goto: entrypoint
|
||||
- What are your plans for the future?:
|
||||
- Weeeeeeeeeeeeeeeeeeeee!
|
||||
- Gonna tune up my thrusters past the limit!
|
||||
- I wanna get seriously squished when I put my hooves to the floor!
|
||||
- And, uhm, maybe do that mineral survey at some point.
|
||||
- if ~$geologist:
|
||||
- I game out here to work as a geologist...
|
||||
- set: geologist
|
||||
- But that can wait, there's FLYING to be done!
|
||||
- goto: entrypoint
|
||||
- if: $pig
|
||||
You're pork! Yummy! I'm starving!:
|
||||
- label: pork
|
||||
- set: $annoyed
|
||||
- set: $helmet
|
||||
- Ohhh, 屎蛋 tree dweller is starving!
|
||||
- Your recycler stopped pumping your own 屎 up your feeding tube?
|
||||
- Ok, here's the deal. I'll give you a good snack.
|
||||
- But the helmet is blocking your mouth, right?
|
||||
- So first, open the space suit menu [press ESC] and take off your helmet.
|
||||
- goto: EXIT
|
||||
- Gotta go!:
|
||||
- Bye!
|
||||
- goto: EXIT
|
||||
- goto: EXIT
|
||||
|
||||
# Oxygen Handler
|
||||
# ===================
|
||||
- label: generic_needoxygen
|
||||
- if $annoyed:
|
||||
- And you want my help?
|
||||
- Help from the weird looking person?
|
||||
- I guess I can share some, here you go.
|
||||
- script: refilloxygen 0.002
|
||||
- if ~$annoyed:
|
||||
- Nooooooooo!!!
|
||||
- Take some of mine!
|
||||
- script: refilloxygen 0.01
|
||||
- Phew, that was close. Take care of yourself!
|
||||
- Anything else?
|
||||
- goto: entrypoint
|
||||
|
|
192
src/cmd.rs
192
src/cmd.rs
|
@ -27,13 +27,22 @@ pub struct CmdPlugin;
|
|||
impl Plugin for CmdPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, load_defs);
|
||||
app.add_systems(
|
||||
Update,
|
||||
handle_spawn_events
|
||||
.before(spawn_entities)
|
||||
.before(spawn_scenes),
|
||||
);
|
||||
app.add_systems(Update, spawn_entities);
|
||||
app.add_systems(Update, spawn_scenes.after(spawn_entities));
|
||||
app.add_systems(Update, process_mesh);
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
hide_colliders.run_if(any_with_component::<NeedsSceneColliderRemoved>),
|
||||
);
|
||||
app.add_event::<SpawnEvent>();
|
||||
app.add_event::<SpawnActorEvent>();
|
||||
app.add_event::<SpawnSceneEvent>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,9 +50,14 @@ impl Plugin for CmdPlugin {
|
|||
pub struct NeedsSceneColliderRemoved;
|
||||
#[derive(Event)]
|
||||
pub struct SpawnEvent(ParserState);
|
||||
#[derive(Event)]
|
||||
pub struct SpawnActorEvent(ParserState);
|
||||
#[derive(Event)]
|
||||
pub struct SpawnSceneEvent(ParserState);
|
||||
#[derive(PartialEq, Clone)]
|
||||
enum DefClass {
|
||||
Actor,
|
||||
Scene,
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -86,6 +100,7 @@ struct ParserState {
|
|||
wants_maxrotation: Option<f64>,
|
||||
wants_maxvelocity: Option<f64>,
|
||||
wants_tolookat_id: Option<String>,
|
||||
wants_matchvelocity_id: Option<String>,
|
||||
collider_is_mesh: bool,
|
||||
collider_is_one_mesh_of_scene: bool,
|
||||
thrust_forward: f32,
|
||||
|
@ -143,6 +158,7 @@ impl Default for ParserState {
|
|||
wants_maxrotation: None,
|
||||
wants_maxvelocity: None,
|
||||
wants_tolookat_id: None,
|
||||
wants_matchvelocity_id: None,
|
||||
collider_is_mesh: false,
|
||||
collider_is_one_mesh_of_scene: false,
|
||||
thrust_forward: default_engine.thrust_forward,
|
||||
|
@ -278,6 +294,42 @@ pub fn load_defs(mut ew_spawn: EventWriter<SpawnEvent>) {
|
|||
// command: pointofinterest yes
|
||||
state.is_point_of_interest = true;
|
||||
}
|
||||
["template", "cruiser"] => {
|
||||
// command: actor ? ? ? cruiser
|
||||
state.class = DefClass::Actor;
|
||||
state.model = Some("cruiser".to_string());
|
||||
|
||||
// command: scale 5
|
||||
state.model_scale = 5.0;
|
||||
|
||||
// command: vehicle yes
|
||||
state.is_vehicle = true;
|
||||
|
||||
// command: angularmomentum 0 0 0
|
||||
state.angular_momentum = DVec3::ZERO;
|
||||
|
||||
// command: collider handcrafted
|
||||
state.collider_is_one_mesh_of_scene = true;
|
||||
|
||||
// command: thrust 16 16 8 100000 3
|
||||
state.thrust_forward = 16.0;
|
||||
state.thrust_back = 16.0;
|
||||
state.thrust_sideways = 8.0;
|
||||
state.reaction_wheels = 100000.0;
|
||||
state.warmup_seconds = 3.0;
|
||||
|
||||
// command: engine ion
|
||||
state.engine_type = actor::EngineType::Ion;
|
||||
|
||||
// command: camdistance 50
|
||||
state.camdistance = 50.0;
|
||||
|
||||
// command: density 500
|
||||
state.density = 500.0;
|
||||
|
||||
// command: pointofinterest yes
|
||||
state.is_point_of_interest = true;
|
||||
}
|
||||
|
||||
// Parsing actors
|
||||
["actor", x, y, z, model] => {
|
||||
|
@ -309,6 +361,21 @@ pub fn load_defs(mut ew_spawn: EventWriter<SpawnEvent>) {
|
|||
continue;
|
||||
}
|
||||
}
|
||||
["scene", x, y, z, name] => {
|
||||
ew_spawn.send(SpawnEvent(state));
|
||||
state = ParserState::default();
|
||||
state.class = DefClass::Scene;
|
||||
state.name = Some(name.to_string());
|
||||
if let (Ok(x_float), Ok(y_float), Ok(z_float)) =
|
||||
(x.parse::<f64>(), y.parse::<f64>(), z.parse::<f64>())
|
||||
{
|
||||
state.pos = DVec3::new(x_float, y_float, z_float);
|
||||
} else {
|
||||
error!("Can't parse coordinates as floats in def: {line}");
|
||||
state = ParserState::default();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
["relativeto", id] => {
|
||||
state.relative_to = Some(id.to_string());
|
||||
}
|
||||
|
@ -582,6 +649,10 @@ pub fn load_defs(mut ew_spawn: EventWriter<SpawnEvent>) {
|
|||
// NOTE: Will not work if the actor has no engine
|
||||
state.wants_tolookat_id = Some(id.to_string());
|
||||
}
|
||||
["wants", "matchvelocitywith", id] => {
|
||||
// NOTE: Will not work if the actor has no engine
|
||||
state.wants_matchvelocity_id = Some(id.to_string());
|
||||
}
|
||||
["armodel", asset_name] => {
|
||||
state.ar_model = Some(asset_name.to_string());
|
||||
}
|
||||
|
@ -604,8 +675,94 @@ pub fn load_defs(mut ew_spawn: EventWriter<SpawnEvent>) {
|
|||
ew_spawn.send(SpawnEvent(state));
|
||||
}
|
||||
|
||||
fn spawn_entities(
|
||||
fn handle_spawn_events(
|
||||
mut er_spawn: EventReader<SpawnEvent>,
|
||||
mut ew_spawnscene: EventWriter<SpawnSceneEvent>,
|
||||
mut ew_spawnactor: EventWriter<SpawnActorEvent>,
|
||||
) {
|
||||
for state in er_spawn.read() {
|
||||
match state.0.class {
|
||||
DefClass::Actor => {
|
||||
ew_spawnactor.send(SpawnActorEvent(state.0.clone()));
|
||||
}
|
||||
DefClass::Scene => {
|
||||
ew_spawnscene.send(SpawnSceneEvent(state.0.clone()));
|
||||
}
|
||||
DefClass::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_scenes(
|
||||
mut er_spawnscene: EventReader<SpawnSceneEvent>,
|
||||
mut ew_spawn: EventWriter<SpawnEvent>,
|
||||
) {
|
||||
for state_wrapper in er_spawnscene.read() {
|
||||
let root_state = &state_wrapper.0;
|
||||
|
||||
let scene_defs = include!("data/scenes.in");
|
||||
for (name, template, pos, rot) in scene_defs {
|
||||
if Some(name.to_string()) == root_state.name {
|
||||
match template {
|
||||
"cruiser" => {
|
||||
let mut state = ParserState::default();
|
||||
state.class = DefClass::Actor;
|
||||
state.pos = DVec3::new(
|
||||
root_state.pos[0] + pos[0],
|
||||
root_state.pos[1] - pos[2],
|
||||
root_state.pos[2] + pos[1],
|
||||
);
|
||||
state.model = Some("cruiser".to_string());
|
||||
|
||||
state.rotation = Quat::from_euler(EulerRot::XYZ, rot[0], rot[1], rot[2]);
|
||||
|
||||
// command: relativeto ?
|
||||
state.relative_to = root_state.relative_to.clone();
|
||||
|
||||
// command: name Cruiser
|
||||
state.name = Some("Cruiser".to_string());
|
||||
|
||||
// command: scale 5
|
||||
state.model_scale = 5.0;
|
||||
|
||||
// command: vehicle yes
|
||||
state.is_vehicle = true;
|
||||
|
||||
// command: angularmomentum 0 0 0
|
||||
state.angular_momentum = DVec3::ZERO;
|
||||
|
||||
// command: collider handcrafted
|
||||
state.collider_is_one_mesh_of_scene = true;
|
||||
|
||||
// command: thrust 16 16 8 100000 3
|
||||
state.thrust_forward = 16.0;
|
||||
state.thrust_back = 16.0;
|
||||
state.thrust_sideways = 8.0;
|
||||
state.reaction_wheels = 100000.0;
|
||||
state.warmup_seconds = 3.0;
|
||||
|
||||
// command: engine ion
|
||||
state.engine_type = actor::EngineType::Ion;
|
||||
|
||||
// command: camdistance 50
|
||||
state.camdistance = 50.0;
|
||||
|
||||
// command: density 500
|
||||
state.density = 500.0;
|
||||
|
||||
// command: pointofinterest yes
|
||||
state.is_point_of_interest = true;
|
||||
ew_spawn.send(SpawnEvent(state));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_entities(
|
||||
mut er_spawn: EventReader<SpawnActorEvent>,
|
||||
mut commands: Commands,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
|
@ -617,9 +774,15 @@ fn spawn_entities(
|
|||
settings: Res<var::Settings>,
|
||||
) {
|
||||
for state_wrapper in er_spawn.read() {
|
||||
let jupiter_pos: DVec3 = if let Some(jupiter_pos) = id2pos.0.get(ID_JUPITER) {
|
||||
*jupiter_pos
|
||||
} else {
|
||||
warn!("Could not determine Jupiter's position");
|
||||
DVec3::ZERO
|
||||
};
|
||||
let state = &state_wrapper.0;
|
||||
let mut rotation = state.rotation;
|
||||
if state.class == DefClass::Actor {
|
||||
|
||||
// Preprocessing
|
||||
let mut absolute_pos = if let Some(id) = &state.relative_to {
|
||||
match id2pos.0.get(&id.to_string()) {
|
||||
|
@ -647,9 +810,8 @@ fn spawn_entities(
|
|||
}
|
||||
};
|
||||
let orbital_period = nature::simple_orbital_period(mass, r);
|
||||
phase_radians += if let Ok(epoch) =
|
||||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
|
||||
{
|
||||
phase_radians +=
|
||||
if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
||||
let now = epoch.as_secs_f64() + 614533234154.0; // random
|
||||
PI * 2.0 * (now % orbital_period) / orbital_period
|
||||
} else {
|
||||
|
@ -668,6 +830,13 @@ fn spawn_entities(
|
|||
1.0
|
||||
} * state.model_scale,
|
||||
);
|
||||
let orbits_jupiter = state.id != ID_JUPITER;
|
||||
let velocity = if orbits_jupiter {
|
||||
let coords = absolute_pos - jupiter_pos;
|
||||
state.velocity + nature::orbital_velocity(coords, nature::JUPITER_MASS)
|
||||
} else {
|
||||
state.velocity
|
||||
};
|
||||
|
||||
// Spawn the actor
|
||||
let actor_entity;
|
||||
|
@ -680,6 +849,9 @@ fn spawn_entities(
|
|||
..default()
|
||||
});
|
||||
actor.insert(SleepingDisabled);
|
||||
if orbits_jupiter {
|
||||
actor.insert(actor::OrbitsJupiter);
|
||||
}
|
||||
actor.insert(world::DespawnOnPlayerDeath);
|
||||
actor.insert(actor::HitPoints::default());
|
||||
actor.insert(Position::from(absolute_pos));
|
||||
|
@ -715,7 +887,7 @@ fn spawn_entities(
|
|||
// Physics Parameters
|
||||
if state.has_physics {
|
||||
actor.insert(RigidBody::Dynamic);
|
||||
actor.insert(LinearVelocity(state.velocity));
|
||||
actor.insert(LinearVelocity(velocity));
|
||||
actor.insert(AngularVelocity(state.angular_momentum));
|
||||
actor.insert(ColliderDensity(state.density));
|
||||
if state.collider_is_mesh {
|
||||
|
@ -799,6 +971,9 @@ fn spawn_entities(
|
|||
if let Some(value) = &state.wants_tolookat_id {
|
||||
actor.insert(actor::WantsToLookAt(value.clone()));
|
||||
}
|
||||
if let Some(value) = &state.wants_matchvelocity_id {
|
||||
actor.insert(actor::WantsMatchVelocityWith(value.clone()));
|
||||
}
|
||||
if let Some(color) = state.light_color {
|
||||
actor.insert((
|
||||
PointLight {
|
||||
|
@ -923,8 +1098,8 @@ fn spawn_entities(
|
|||
}
|
||||
|
||||
if state.has_ring {
|
||||
let ring_radius = state.model_scale
|
||||
* (nature::JUPITER_RING_RADIUS / nature::JUPITER_RADIUS) as f32;
|
||||
let ring_radius =
|
||||
state.model_scale * (nature::JUPITER_RING_RADIUS / nature::JUPITER_RADIUS) as f32;
|
||||
commands.spawn((
|
||||
world::DespawnOnPlayerDeath,
|
||||
MaterialMeshBundle {
|
||||
|
@ -944,7 +1119,6 @@ fn spawn_entities(
|
|||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_colliders(
|
||||
|
|
|
@ -122,11 +122,11 @@ actor 0 0 0
|
|||
only_in_map_at_dist 1e7 amalthea
|
||||
clickable no
|
||||
physics off
|
||||
actor 0 127093 0 moonlet
|
||||
actor 0 0 0 moonlet
|
||||
name Thebe
|
||||
relativeto jupiter
|
||||
id thebe
|
||||
orbit 221900e3 0.34
|
||||
orbitaround jupiter 221900e3
|
||||
scale 50e3
|
||||
moon yes
|
||||
angularmomentum 0 0.025 0
|
||||
|
@ -256,10 +256,11 @@ actor 0 0 0
|
|||
physics off
|
||||
|
||||
|
||||
actor 0 593051 0 suitv2
|
||||
actor 0 59305 0 suitv2
|
||||
template person
|
||||
relativeto jupiter
|
||||
orbit 221900e3 0.338
|
||||
orbitaround jupiter 221900e3
|
||||
orbit_phase_offset 0.002
|
||||
player yes
|
||||
id player
|
||||
wants maxvelocity none
|
||||
|
@ -267,18 +268,8 @@ actor 0 593051 0 suitv2
|
|||
health 0.3
|
||||
rotationy 135
|
||||
|
||||
actor 10 -30 20 cruiser
|
||||
name "Cruiser"
|
||||
scene 10 -30 20 test
|
||||
relativeto player
|
||||
scale 5
|
||||
vehicle yes
|
||||
collider handcrafted
|
||||
thrust 16 16 8 100000 3
|
||||
engine ion
|
||||
camdistance 50
|
||||
density 500
|
||||
angularmomentum 0.1 0.1 0.3
|
||||
pointofinterest yes
|
||||
|
||||
actor -55e3 44e3 0 suitv2
|
||||
template person
|
||||
|
@ -287,6 +278,7 @@ actor -55e3 44e3 0 suitv2
|
|||
name "Yuni"
|
||||
chatid Yuni
|
||||
rotationx 180
|
||||
wants matchvelocitywith thebe
|
||||
|
||||
actor 5000 0 -3000 moonlet
|
||||
name Moonlet
|
||||
|
@ -308,12 +300,14 @@ actor 13200 300 -3000 hollow_asteroid
|
|||
actor 0 0 0 suitv2
|
||||
template person
|
||||
relativeto cultasteroid
|
||||
wants matchvelocitywith cultasteroid
|
||||
name "Ash"
|
||||
chatid Ash
|
||||
pronoun they
|
||||
actor -8 8 0 suitv2
|
||||
template person
|
||||
relativeto cultasteroid
|
||||
wants matchvelocitywith cultasteroid
|
||||
name "River"
|
||||
chatid River
|
||||
rotationy 54
|
||||
|
@ -420,12 +414,25 @@ actor -3300 10 0 pizzeria
|
|||
actor 60 60 -23 pizzasign
|
||||
name "Pizzeria Sign"
|
||||
relativeto pizzeria
|
||||
id pizzeriasign
|
||||
scale 20
|
||||
collider mesh
|
||||
density 200
|
||||
rotationy 81
|
||||
angularmomentum 0 0 0
|
||||
light "FF00B3" 30000000
|
||||
actor 18 22 -15 sus
|
||||
template person
|
||||
relativeto pizzeriasign
|
||||
name Sus
|
||||
id Sus
|
||||
chatid Sus
|
||||
angularmomentum 0.4 0.2 0.1
|
||||
wants maxrotation 0.2
|
||||
wants matchvelocitywith pizzeria
|
||||
rotationy 108
|
||||
rotationx 180
|
||||
pronoun he
|
||||
actor -52 -10 0 lightorb
|
||||
name "Light Orb"
|
||||
relativeto pizzeria
|
||||
|
@ -442,6 +449,7 @@ actor -3300 10 0 pizzeria
|
|||
relativeto pizzeria
|
||||
armodel clippy_ar
|
||||
wants lookat PLAYERCAMERA
|
||||
wants matchvelocitywith pizzeria
|
||||
rotationy -126
|
||||
chatid SubduedClippy
|
||||
|
||||
|
@ -452,6 +460,7 @@ actor -3300 10 0 pizzeria
|
|||
chatid PizzaChef
|
||||
armodel suit_ar_chefhat
|
||||
wants lookat PLAYERCAMERA
|
||||
wants matchvelocitywith pizzeria
|
||||
rotationy -90
|
||||
pronoun he
|
||||
|
||||
|
@ -464,6 +473,7 @@ actor 30 -12 -40 suitv2
|
|||
armodel suit_ar_wings
|
||||
angularmomentum 0.4 0.2 0.1
|
||||
wants maxrotation 0.5
|
||||
wants matchvelocitywith pizzeria
|
||||
rotationy 108
|
||||
rotationx 180
|
||||
pronoun it
|
||||
|
@ -495,6 +505,7 @@ actor -300 0 40 suitv2
|
|||
name "梓涵"
|
||||
chatid Drifter
|
||||
alive no
|
||||
wants maxvelocity none
|
||||
oxygen 0.08
|
||||
pronoun she
|
||||
|
||||
|
@ -505,12 +516,14 @@ actor 100 -18000 2000 clippy
|
|||
name "StarTrans Clippy™ Serenity Station"
|
||||
armodel clippy_ar
|
||||
wants lookat PLAYERCAMERA
|
||||
wants matchvelocitywith orbbusstopserenity
|
||||
rotationy -90
|
||||
chatid ClippyTransSerenity
|
||||
|
||||
actor 60 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Serenity Station"
|
||||
relativeto busstopclippy
|
||||
id orbbusstopserenity
|
||||
scale 5
|
||||
actor 80 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Serenity Station"
|
||||
|
@ -637,6 +650,7 @@ actor 100 -18000 2000 clippy
|
|||
actor 8 20 0 suitv2
|
||||
template person
|
||||
relativeto "busstopclippy"
|
||||
wants matchvelocitywith orbbusstopserenity
|
||||
name "Rudy"
|
||||
chatid NPCinCryoStasis
|
||||
pronoun he
|
||||
|
@ -648,12 +662,14 @@ actor -184971e3 149410e3 -134273e3 clippy
|
|||
name "StarTrans Clippy™ Farview Station"
|
||||
armodel clippy_ar
|
||||
wants lookat PLAYERCAMERA
|
||||
wants matchvelocitywith orbbusstopfarview
|
||||
rotationy -90
|
||||
chatid ClippyTransFarview
|
||||
|
||||
actor 60 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Farview Station"
|
||||
relativeto busstopclippy2
|
||||
id orbbusstopfarview
|
||||
scale 5
|
||||
actor 80 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Farview Station"
|
||||
|
@ -785,6 +801,7 @@ actor 0 -44e3 0 clippy
|
|||
name "StarTrans Clippy™ Metis Prime Station"
|
||||
armodel clippy_ar
|
||||
wants lookat PLAYERCAMERA
|
||||
wants matchvelocitywith orbbusstopmetis
|
||||
orbitaround jupiter 128000e3
|
||||
orbit_phase_offset -0.002
|
||||
rotationy -90
|
||||
|
@ -793,6 +810,7 @@ actor 0 -44e3 0 clippy
|
|||
actor 60 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Metis Prime Station"
|
||||
relativeto busstopclippy3
|
||||
id orbbusstopmetis
|
||||
scale 5
|
||||
actor 80 0 0 "orb_busstop"
|
||||
name "StarTrans Bus Stop: Metis Prime Station"
|
||||
|
|
|
@ -14,8 +14,19 @@
|
|||
# fullscreen_mode may be "borderless", "legacy", or "sized"
|
||||
fullscreen_mode = "borderless"
|
||||
|
||||
# window_mode may be "windowed", or "fullscreen"
|
||||
window_mode = "fullscreen"
|
||||
# fullscreen_on may be true or false
|
||||
fullscreen_on = true
|
||||
|
||||
# render_mode may be "vulkan" or "gl"
|
||||
render_mode = "vulkan"
|
||||
|
||||
# radio_station can be an integer number
|
||||
radio_station = 1
|
||||
|
||||
# noise_cancellation_mode can be an integer number
|
||||
noise_cancellation_mode = 0
|
||||
|
||||
# The following options are booleans (may be true or false)
|
||||
augmented_reality = true
|
||||
third_person = true
|
||||
shadows_sun = true
|
||||
|
|
6
src/data/scenes.in
Normal file
6
src/data/scenes.in
Normal file
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
("test", "cruiser", [0.0, 10.971298, 1.222765], [-0.27833763, 0.74558806, 0.31813696]),
|
||||
("test", "cruiser", [-0.46667862, -1.2666901, -7.938822], [1.7932074, 0.10752687, 0.15762906]),
|
||||
("test", "cruiser", [-7.409776, -1.4187636, 11.451159], [-0.27833763, 0.74558806, 0.31813696]),
|
||||
("test", "cruiser", [6.8125467, -11.003204, 0.5323599], [-0.019556522, -1.0606266, -3.0019674]),
|
||||
]
|
137
src/game.rs
137
src/game.rs
|
@ -18,20 +18,31 @@ use bevy::window::{PrimaryWindow, Window, WindowMode};
|
|||
use bevy_xpbd_3d::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const CHEAT_WARP_1: &str = "pizzeria";
|
||||
pub const CHEAT_WARP_2: &str = "busstopclippy2";
|
||||
pub const CHEAT_WARP_3: &str = "busstopclippy3";
|
||||
|
||||
pub struct GamePlugin;
|
||||
impl Plugin for GamePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup);
|
||||
app.add_systems(Update, handle_cheats.run_if(in_control));
|
||||
app.add_systems(Update, debug);
|
||||
app.add_systems(PostUpdate, handle_game_event);
|
||||
app.add_systems(PreUpdate, handle_player_death);
|
||||
app.add_systems(PostUpdate, update_id2pos);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
update_id2pos.in_set(bevy_xpbd_3d::plugins::sync::SyncSet::PositionToTransform),
|
||||
);
|
||||
app.add_systems(PostUpdate, update_id2v);
|
||||
app.add_systems(
|
||||
Update,
|
||||
handle_achievement_event.run_if(on_event::<AchievementEvent>()),
|
||||
);
|
||||
app.add_systems(Update, check_achievements);
|
||||
app.insert_resource(Id2Pos(HashMap::new()));
|
||||
app.insert_resource(Id2V(HashMap::new()));
|
||||
app.insert_resource(JupiterPos(DVec3::ZERO));
|
||||
app.insert_resource(var::AchievementTracker::default());
|
||||
app.insert_resource(var::Settings::default());
|
||||
app.insert_resource(var::GameVars::default());
|
||||
|
@ -50,6 +61,10 @@ pub struct PlayerDiesEvent(pub actor::DamageType);
|
|||
#[derive(Resource)]
|
||||
pub struct Id2Pos(pub HashMap<String, DVec3>);
|
||||
#[derive(Resource)]
|
||||
pub struct Id2V(pub HashMap<String, DVec3>);
|
||||
#[derive(Resource)]
|
||||
pub struct JupiterPos(pub DVec3);
|
||||
#[derive(Resource)]
|
||||
pub struct AchievementCheckTimer(pub Timer);
|
||||
|
||||
#[derive(Event)]
|
||||
|
@ -65,8 +80,8 @@ pub enum AchievementEvent {
|
|||
#[derive(Event)]
|
||||
pub enum GameEvent {
|
||||
SetAR(Turn),
|
||||
SetMusic(Turn),
|
||||
SetSound(Turn),
|
||||
SetMusic(Cycle),
|
||||
SetSound(Cycle),
|
||||
SetMap(Turn),
|
||||
SetFullscreen(Turn),
|
||||
SetMenu(Turn),
|
||||
|
@ -92,6 +107,48 @@ impl Turn {
|
|||
}
|
||||
}
|
||||
|
||||
pub enum Cycle {
|
||||
First,
|
||||
Last,
|
||||
Next,
|
||||
Previous,
|
||||
}
|
||||
|
||||
impl Cycle {
|
||||
pub fn to_index<T>(&self, current_index: usize, vector: &Vec<T>) -> Option<usize> {
|
||||
if vector.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match self {
|
||||
Cycle::First => Some(0),
|
||||
Cycle::Last => Some(vector.len() - 1),
|
||||
Cycle::Next => {
|
||||
let index = current_index.saturating_add(1);
|
||||
if index >= vector.len() {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
}
|
||||
Cycle::Previous => {
|
||||
if current_index == 0 {
|
||||
Some(vector.len() - 1)
|
||||
} else {
|
||||
Some(current_index - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup(mut settings: ResMut<Settings>, prefs: Res<var::Preferences>) {
|
||||
settings.hud_active = prefs.augmented_reality;
|
||||
settings.radio_mode = prefs.radio_station;
|
||||
settings.set_noise_cancellation_mode(prefs.noise_cancellation_mode);
|
||||
settings.third_person = prefs.third_person;
|
||||
settings.shadows_sun = prefs.shadows_sun;
|
||||
}
|
||||
|
||||
pub fn handle_game_event(
|
||||
mut settings: ResMut<Settings>,
|
||||
mut er_game: EventReader<GameEvent>,
|
||||
|
@ -104,6 +161,7 @@ pub fn handle_game_event(
|
|||
mut mapcam: ResMut<camera::MapCam>,
|
||||
mut log: ResMut<hud::Log>,
|
||||
opt: Res<var::CommandLineOptions>,
|
||||
mut prefs: ResMut<var::Preferences>,
|
||||
) {
|
||||
for event in er_game.read() {
|
||||
match event {
|
||||
|
@ -111,16 +169,30 @@ pub fn handle_game_event(
|
|||
settings.hud_active = turn.to_bool(settings.hud_active);
|
||||
ew_togglemusic.send(audio::ToggleMusicEvent());
|
||||
ew_updateoverlays.send(hud::UpdateOverlayVisibility);
|
||||
prefs.augmented_reality = settings.hud_active;
|
||||
prefs.save();
|
||||
}
|
||||
GameEvent::SetMusic(turn) => {
|
||||
// TODO invert "mute_music" to "music_active"
|
||||
settings.mute_music = turn.to_bool(settings.mute_music);
|
||||
ew_togglemusic.send(audio::ToggleMusicEvent());
|
||||
GameEvent::SetMusic(cycle) => {
|
||||
match cycle.to_index(settings.radio_mode, &settings.radio_modes) {
|
||||
Some(mode) => {
|
||||
settings.radio_mode = mode;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
GameEvent::SetSound(turn) => {
|
||||
// TODO invert "mute_sfx" to "sfx_active"
|
||||
settings.mute_sfx = turn.to_bool(settings.mute_sfx);
|
||||
ew_togglemusic.send(audio::ToggleMusicEvent());
|
||||
prefs.radio_station = settings.radio_mode;
|
||||
prefs.save();
|
||||
}
|
||||
GameEvent::SetSound(cycle) => {
|
||||
match cycle.to_index(settings.noise_cancellation_mode, &settings.noise_cancellation_modes) {
|
||||
Some(mode) => {
|
||||
settings.set_noise_cancellation_mode(mode);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
ew_togglemusic.send(audio::ToggleMusicEvent());
|
||||
prefs.noise_cancellation_mode = settings.noise_cancellation_mode;
|
||||
prefs.save();
|
||||
}
|
||||
GameEvent::SetMap(turn) => {
|
||||
settings.map_active = turn.to_bool(settings.map_active);
|
||||
|
@ -133,10 +205,12 @@ pub fn handle_game_event(
|
|||
GameEvent::SetFullscreen(turn) => {
|
||||
for mut window in &mut q_window {
|
||||
let current_state = window.mode != WindowMode::Windowed;
|
||||
window.mode = match turn.to_bool(current_state) {
|
||||
prefs.fullscreen_on = turn.to_bool(current_state);
|
||||
window.mode = match prefs.fullscreen_on {
|
||||
true => opt.window_mode_fullscreen,
|
||||
false => WindowMode::Windowed,
|
||||
};
|
||||
prefs.save();
|
||||
}
|
||||
}
|
||||
GameEvent::SetMenu(turn) => {
|
||||
|
@ -145,6 +219,8 @@ pub fn handle_game_event(
|
|||
}
|
||||
GameEvent::SetThirdPerson(turn) => {
|
||||
settings.third_person = turn.to_bool(settings.third_person);
|
||||
prefs.third_person = settings.third_person;
|
||||
prefs.save();
|
||||
}
|
||||
GameEvent::SetRotationStabilizer(turn) => {
|
||||
settings.rotation_stabilizer_active =
|
||||
|
@ -155,6 +231,8 @@ pub fn handle_game_event(
|
|||
for mut light in &mut q_light {
|
||||
light.shadows_enabled = settings.shadows_sun;
|
||||
}
|
||||
prefs.shadows_sun = settings.shadows_sun;
|
||||
prefs.save();
|
||||
}
|
||||
GameEvent::Achievement(name) => {
|
||||
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Achieve));
|
||||
|
@ -260,7 +338,9 @@ fn handle_cheats(
|
|||
>,
|
||||
mut ew_playerdies: EventWriter<PlayerDiesEvent>,
|
||||
mut settings: ResMut<Settings>,
|
||||
jupiter_pos: Res<JupiterPos>,
|
||||
id2pos: Res<Id2Pos>,
|
||||
id2v: Res<Id2V>,
|
||||
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
|
||||
) {
|
||||
if q_player.is_empty() || q_life.is_empty() {
|
||||
|
@ -285,7 +365,7 @@ fn handle_cheats(
|
|||
|
||||
if key_input.just_pressed(settings.key_cheat_stop) {
|
||||
gforce.ignore_gforce_seconds = 1.0;
|
||||
v.0 = DVec3::ZERO;
|
||||
v.0 = nature::orbital_velocity(pos.0 - jupiter_pos.0, nature::JUPITER_MASS);
|
||||
}
|
||||
if key_input.pressed(settings.key_cheat_speed)
|
||||
|| key_input.pressed(settings.key_cheat_speed_backward)
|
||||
|
@ -319,22 +399,31 @@ fn handle_cheats(
|
|||
}
|
||||
|
||||
if key_input.just_pressed(settings.key_cheat_pizza) {
|
||||
if let Some(target) = id2pos.0.get(&"pizzeria".to_string()) {
|
||||
if let Some(target) = id2pos.0.get(&CHEAT_WARP_1.to_string()) {
|
||||
pos.0 = *target + DVec3::new(-60.0, 0.0, 0.0);
|
||||
gforce.ignore_gforce_seconds = 1.0;
|
||||
}
|
||||
if let Some(target) = id2v.0.get(&CHEAT_WARP_1.to_string()) {
|
||||
v.0 = *target;
|
||||
}
|
||||
}
|
||||
if key_input.just_pressed(settings.key_cheat_farview1) {
|
||||
if let Some(target) = id2pos.0.get(&"busstopclippy2".to_string()) {
|
||||
if let Some(target) = id2pos.0.get(&CHEAT_WARP_2.to_string()) {
|
||||
pos.0 = *target + DVec3::new(0.0, -1000.0, 0.0);
|
||||
gforce.ignore_gforce_seconds = 1.0;
|
||||
}
|
||||
if let Some(target) = id2v.0.get(&CHEAT_WARP_2.to_string()) {
|
||||
v.0 = *target;
|
||||
}
|
||||
}
|
||||
if key_input.just_pressed(settings.key_cheat_farview2) {
|
||||
if let Some(target) = id2pos.0.get(&"busstopclippy3".to_string()) {
|
||||
if let Some(target) = id2pos.0.get(&CHEAT_WARP_3.to_string()) {
|
||||
pos.0 = *target + DVec3::new(0.0, -1000.0, 0.0);
|
||||
gforce.ignore_gforce_seconds = 1.0;
|
||||
}
|
||||
if let Some(target) = id2v.0.get(&CHEAT_WARP_3.to_string()) {
|
||||
v.0 = *target;
|
||||
}
|
||||
}
|
||||
if key_input.pressed(settings.key_cheat_adrenaline_zero) {
|
||||
lifeform.adrenaline = 0.0;
|
||||
|
@ -351,10 +440,24 @@ fn handle_cheats(
|
|||
}
|
||||
}
|
||||
|
||||
fn update_id2pos(mut id2pos: ResMut<Id2Pos>, q_id: Query<(&Position, &actor::Identifier)>) {
|
||||
fn update_id2pos(
|
||||
mut id2pos: ResMut<Id2Pos>,
|
||||
mut jupiterpos: ResMut<JupiterPos>,
|
||||
q_id: Query<(&Position, &actor::Identifier)>,
|
||||
) {
|
||||
id2pos.0.clear();
|
||||
for (pos, id) in &q_id {
|
||||
id2pos.0.insert(id.0.clone(), pos.0);
|
||||
if id.0 == "jupiter" {
|
||||
jupiterpos.0 = pos.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_id2v(mut id2v: ResMut<Id2V>, q_id: Query<(&LinearVelocity, &actor::Identifier)>) {
|
||||
id2v.0.clear();
|
||||
for (v, id) in &q_id {
|
||||
id2v.0.insert(id.0.clone(), v.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -366,6 +469,7 @@ fn debug(
|
|||
Assets<ExtendedMaterial<StandardMaterial, load::AsteroidSurface>>,
|
||||
>,
|
||||
mut achievement_tracker: ResMut<var::AchievementTracker>,
|
||||
vars: Res<var::GameVars>,
|
||||
materials: Query<(Entity, Option<&Name>, &Handle<Mesh>)>,
|
||||
) {
|
||||
if settings.dev_mode && keyboard_input.just_pressed(KeyCode::KeyP) {
|
||||
|
@ -379,6 +483,7 @@ fn debug(
|
|||
}
|
||||
if settings.dev_mode && keyboard_input.just_pressed(KeyCode::KeyN) {
|
||||
achievement_tracker.achieve_all();
|
||||
dbg!(&vars);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -707,7 +707,8 @@ fn update_dashboard(
|
|||
fn update_speedometer(
|
||||
timer: ResMut<FPSUpdateTimer>,
|
||||
settings: Res<Settings>,
|
||||
q_camera: Query<&LinearVelocity, With<actor::PlayerCamera>>,
|
||||
jupiter_pos: Res<game::JupiterPos>,
|
||||
q_camera: Query<(&LinearVelocity, &Position), With<actor::PlayerCamera>>,
|
||||
q_player: Query<&actor::ExperiencesGForce, With<actor::Player>>,
|
||||
q_target: Query<&LinearVelocity, With<IsTargeted>>,
|
||||
mut q_speedometer: Query<&mut Style, (With<Speedometer>, Without<Speedometer2>)>,
|
||||
|
@ -717,8 +718,9 @@ fn update_speedometer(
|
|||
if !settings.hud_active || !timer.0.just_finished() {
|
||||
return;
|
||||
}
|
||||
if let Ok(cam_v) = q_camera.get_single() {
|
||||
let speed = cam_v.length();
|
||||
if let Ok((cam_v, pos)) = q_camera.get_single() {
|
||||
let orbital_v = nature::orbital_velocity(pos.0 - jupiter_pos.0, nature::JUPITER_MASS);
|
||||
let speed = (cam_v.0 - orbital_v).length();
|
||||
|
||||
let speedometer_split = 5_000.0;
|
||||
if let Ok(mut speedometer) = q_speedometer.get_single_mut() {
|
||||
|
|
|
@ -31,6 +31,7 @@ pub fn asset_name_to_path(name: &str) -> &'static str {
|
|||
"suitv2" => "models/suit_v2/suit_v2.glb#Scene0",
|
||||
"suit_ar_chefhat" => "models/suit_v2/ar_chefhat.glb#Scene0",
|
||||
"suit_ar_wings" => "models/suit_v2/ar_wings.glb#Scene0",
|
||||
"sus" => "models/sus.glb#Scene0",
|
||||
"asteroid1" => "models/asteroid.glb#Scene0",
|
||||
"asteroid2" => "models/asteroid2.glb#Scene0",
|
||||
"asteroid_lum" => "models/asteroid_lum.glb#Scene0",
|
||||
|
|
|
@ -35,6 +35,7 @@ pub mod prelude {
|
|||
actor, audio, camera, chat, cmd, common, game, hud, load, menu, nature, var, visual, world,
|
||||
};
|
||||
pub use game::Turn::Toggle;
|
||||
pub use game::Cycle::Next;
|
||||
pub use game::{GameEvent, Turn};
|
||||
}
|
||||
|
||||
|
@ -104,6 +105,8 @@ fn main() {
|
|||
env::set_var("WGPU_BACKEND", "gl");
|
||||
}
|
||||
|
||||
dbg!(&prefs);
|
||||
|
||||
let mut app = App::new();
|
||||
app.insert_resource(opt);
|
||||
|
||||
|
|
24
src/menu.rs
24
src/menu.rs
|
@ -458,12 +458,24 @@ pub fn update_menu(
|
|||
|
||||
match MENUDEF[i].1 {
|
||||
MenuAction::ToggleSound => {
|
||||
let onoff = bool2string(!settings.mute_sfx);
|
||||
text.sections[i].value = format!("Sound: {onoff}\n");
|
||||
let noisecancel =
|
||||
if let Some(noisecancel) =
|
||||
settings.noise_cancellation_modes.get(settings.noise_cancellation_mode)
|
||||
{
|
||||
noisecancel
|
||||
} else {
|
||||
&settings.noise_cancellation_modes[0]
|
||||
};
|
||||
text.sections[i].value = format!("Noise Cancellation: {noisecancel}\n");
|
||||
}
|
||||
MenuAction::ToggleMusic => {
|
||||
let onoff = bool2string(!settings.mute_music);
|
||||
text.sections[i].value = format!("Music: {onoff}\n");
|
||||
let station =
|
||||
if let Some(station) = settings.radio_modes.get(settings.radio_mode) {
|
||||
station
|
||||
} else {
|
||||
&settings.radio_modes[0]
|
||||
};
|
||||
text.sections[i].value = format!("Radio: {station}\n");
|
||||
}
|
||||
MenuAction::ToggleAR => {
|
||||
let onoff = bool2string(settings.hud_active);
|
||||
|
@ -566,11 +578,11 @@ pub fn handle_input(
|
|||
ew_updatemenu.send(UpdateMenuEvent);
|
||||
}
|
||||
MenuAction::ToggleMusic => {
|
||||
ew_game.send(GameEvent::SetMusic(Toggle));
|
||||
ew_game.send(GameEvent::SetMusic(Next));
|
||||
ew_updatemenu.send(UpdateMenuEvent);
|
||||
}
|
||||
MenuAction::ToggleSound => {
|
||||
ew_game.send(GameEvent::SetSound(Toggle));
|
||||
ew_game.send(GameEvent::SetSound(Next));
|
||||
ew_updatemenu.send(UpdateMenuEvent);
|
||||
}
|
||||
MenuAction::ToggleCamera => {
|
||||
|
|
|
@ -27,9 +27,11 @@ pub const G: f64 = 6.6743015e-11; // Gravitational constant in Nm²/kg²
|
|||
pub const SOL_RADIUS: f64 = 696_300_000.0;
|
||||
pub const JUPITER_RADIUS: f64 = 71_492_000.0;
|
||||
pub const JUPITER_RING_RADIUS: f64 = 229_000_000.0;
|
||||
pub const EARTH_RADIUS: f64 = 6_371_000.0;
|
||||
|
||||
pub const SOL_MASS: f64 = 1.9885e30;
|
||||
pub const JUPITER_MASS: f64 = 1.8982e27;
|
||||
pub const EARTH_MASS: f64 = 5.972168e24;
|
||||
|
||||
// Each star's values: (x, y, z, magnitude, color index, distance, name)
|
||||
pub const STARS: &[(f32, f32, f32, f32, f32, f32, &str)] = &include!("data/stars.in");
|
||||
|
@ -163,10 +165,38 @@ pub fn inverse_lorentz_factor_custom_c(speed: f64, c: f64) -> f64 {
|
|||
(1.0 - (speed.powf(2.0) / c.powf(2.0))).sqrt()
|
||||
}
|
||||
|
||||
/// Calculates orbit duration in seconds, with given parameters, assuming circular orbit.
|
||||
pub fn simple_orbital_period(mass: f64, distance: f64) -> f64 {
|
||||
return 2.0 * PI * (distance.powf(3.0) / (G * mass)).sqrt();
|
||||
}
|
||||
|
||||
/// Calculates the orbital velocity with given parameters, assuming prograde circular orbit.
|
||||
pub fn orbital_velocity(coords: DVec3, mass: f64) -> DVec3 {
|
||||
let r = coords.length();
|
||||
let speed = (G * mass / r).sqrt();
|
||||
|
||||
// This generates a perpendicular orbital vector in the prograde direction
|
||||
let perpendicular = DVec3::new(coords.z, 0.0, -coords.x).normalize();
|
||||
|
||||
return perpendicular * speed;
|
||||
}
|
||||
|
||||
/// Calculates the acceleration towards a mass in m/s
|
||||
pub fn gravitational_acceleration(coords: DVec3, mass: f64) -> DVec3 {
|
||||
let r_squared = coords.length_squared();
|
||||
let acceleration_magnitude = G * mass / r_squared;
|
||||
return -acceleration_magnitude * (coords / r_squared.sqrt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gravitational_acceleration() {
|
||||
let coords = DVec3::new(EARTH_RADIUS, 0.0, 0.0);
|
||||
let mass = EARTH_MASS;
|
||||
let g = gravitational_acceleration(coords, mass);
|
||||
let g_rounded = (g * 10.0).round() / 10.0;
|
||||
assert_eq!(g_rounded, DVec3::new(-9.8, 0.0, 0.0));
|
||||
}
|
||||
|
||||
pub fn phase_dist_to_coords(phase_radians: f64, distance: f64) -> DVec3 {
|
||||
return DVec3::new(
|
||||
distance * phase_radians.cos(),
|
||||
|
|
217
src/var.rs
217
src/var.rs
|
@ -14,7 +14,7 @@
|
|||
use crate::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::WindowMode;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
@ -31,6 +31,7 @@ pub const TOKEN_LESS_EQUALS: &str = "<=";
|
|||
pub const TOKEN_NEGATE: &str = "~";
|
||||
|
||||
pub const DEFAULT_CHAT_SPEED: f32 = 10.0;
|
||||
pub const DEFAULT_CONFIG_TOML: &str = include_str!("data/outfly.toml");
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct Settings {
|
||||
|
@ -39,7 +40,10 @@ pub struct Settings {
|
|||
pub version: String,
|
||||
pub alive: bool,
|
||||
pub mute_sfx: bool,
|
||||
pub mute_music: bool,
|
||||
pub noise_cancellation_mode: usize,
|
||||
pub noise_cancellation_modes: Vec<String>,
|
||||
pub radio_mode: usize,
|
||||
pub radio_modes: Vec<String>, // see also: settings.is_radio_playing()
|
||||
pub volume_sfx: u8,
|
||||
pub volume_music: u8,
|
||||
pub mouse_sensitivity: f32,
|
||||
|
@ -154,8 +158,6 @@ pub struct Settings {
|
|||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
let dev_mode = cfg!(feature = "dev_mode") && env::var("CARGO").is_ok();
|
||||
let default_mute_sfx = false;
|
||||
let default_mute_music = dev_mode;
|
||||
let version = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
|
||||
version.to_string()
|
||||
} else {
|
||||
|
@ -167,8 +169,20 @@ impl Default for Settings {
|
|||
god_mode: false,
|
||||
version,
|
||||
alive: true,
|
||||
mute_sfx: default_mute_sfx,
|
||||
mute_music: default_mute_music,
|
||||
mute_sfx: false,
|
||||
noise_cancellation_mode: 0,
|
||||
noise_cancellation_modes: vec![
|
||||
"Off".to_string(),
|
||||
"Ambience".to_string(),
|
||||
"Mechanical".to_string(),
|
||||
"Max".to_string(),
|
||||
],
|
||||
radio_mode: 1,
|
||||
radio_modes: vec![
|
||||
// see also: settings.is_radio_playing()
|
||||
"Off".to_string(),
|
||||
"Cinematic Frequency".to_string(),
|
||||
],
|
||||
volume_sfx: 100,
|
||||
volume_music: 100,
|
||||
mouse_sensitivity: 0.4,
|
||||
|
@ -293,7 +307,6 @@ impl Settings {
|
|||
println!("Resetting player settings!");
|
||||
let default = Self::default();
|
||||
self.rotation_stabilizer_active = default.rotation_stabilizer_active;
|
||||
self.third_person = default.third_person;
|
||||
self.is_zooming = default.is_zooming;
|
||||
self.flashlight_active = default.flashlight_active;
|
||||
self.cruise_control_active = default.cruise_control_active;
|
||||
|
@ -318,6 +331,27 @@ impl Settings {
|
|||
pub fn in_control(&self) -> bool {
|
||||
return self.alive && !self.menu_active;
|
||||
}
|
||||
|
||||
pub fn is_radio_playing(&self, sfx: audio::Sfx) -> Option<bool> {
|
||||
let radio = self.radio_mode;
|
||||
let ambience = self.noise_cancellation_mode != 1 && self.noise_cancellation_mode != 3;
|
||||
match sfx {
|
||||
audio::Sfx::BGM => Some(radio == 1),
|
||||
audio::Sfx::BGMActualJupiterRecording => Some(radio == 0 && ambience),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_noise_cancellation_mode(&mut self, value: usize) {
|
||||
let value = if value >= self.noise_cancellation_modes.len() {
|
||||
warn!("Attempting to set too large noise cancellation mode: {value}");
|
||||
0
|
||||
} else {
|
||||
value
|
||||
};
|
||||
self.noise_cancellation_mode = value;
|
||||
self.mute_sfx = value >= 2;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Debug)]
|
||||
|
@ -411,12 +445,17 @@ impl AchievementTracker {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Deserialize, Debug, Default)]
|
||||
#[derive(Resource, Serialize, Deserialize, Debug, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Preferences {
|
||||
pub fullscreen_mode: String,
|
||||
pub window_mode: String,
|
||||
pub fullscreen_on: bool,
|
||||
pub render_mode: String,
|
||||
pub augmented_reality: bool,
|
||||
pub radio_station: usize,
|
||||
pub noise_cancellation_mode: usize,
|
||||
pub third_person: bool,
|
||||
pub shadows_sun: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
pub source_file: Option<String>,
|
||||
|
@ -431,14 +470,35 @@ impl Preferences {
|
|||
}
|
||||
}
|
||||
pub fn get_window_mode(&self) -> WindowMode {
|
||||
match self.window_mode.as_str() {
|
||||
"fullscreen" => self.get_fullscreen_mode(),
|
||||
_ => WindowMode::Windowed,
|
||||
match self.fullscreen_on {
|
||||
true => self.get_fullscreen_mode(),
|
||||
false => WindowMode::Windowed,
|
||||
}
|
||||
}
|
||||
pub fn render_mode_is_gl(&self) -> bool {
|
||||
return self.render_mode == "gl";
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
if let Some(path) = get_prefs_path() {
|
||||
match toml_edit::ser::to_document::<Preferences>(self) {
|
||||
Ok(doc) => {
|
||||
dbg!(&doc);
|
||||
match fs::write(path.clone(), doc.to_string()) {
|
||||
Ok(_) => {
|
||||
info!("Saved preferences to {path}.");
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Error while writing preferences: {:?}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Error while writing preferences: {:?}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn file_is_readable(file_path: &str) -> bool {
|
||||
|
@ -447,20 +507,47 @@ fn file_is_readable(file_path: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn path_is_directory(file_path: &str) -> bool {
|
||||
fs::metadata(file_path)
|
||||
.map(|metadata| metadata.is_dir())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_prefs_path() -> Option<String> {
|
||||
let test = CONF_FILE;
|
||||
if file_is_readable(test) {
|
||||
return Some(test.to_string());
|
||||
}
|
||||
if let Ok(basedir) = env::var("XDG_CONFIG_HOME") {
|
||||
let test = basedir.to_string() + "/outfly/" + CONF_FILE;
|
||||
if file_is_readable(test.as_str()) {
|
||||
return Some(test);
|
||||
if let Some(mut conf) = dirs::config_dir() {
|
||||
conf.push("OutFly");
|
||||
if !conf.exists() {
|
||||
match fs::create_dir_all(&conf) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
eprintln!("Failed creating configuration directory: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(test) = conf.to_str() {
|
||||
if !path_is_directory(test) {
|
||||
eprintln!("Failed creating configuration directory");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
conf.push(CONF_FILE);
|
||||
if !conf.exists() {
|
||||
match fs::write(&conf, DEFAULT_CONFIG_TOML.to_string()) {
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
eprintln!("Failed creating configuration file: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(test) = conf.to_str() {
|
||||
if file_is_readable(test) {
|
||||
return Some(test.to_string());
|
||||
}
|
||||
} else if let Ok(basedir) = env::var("HOME") {
|
||||
let test = basedir.to_string() + ".config/outfly/" + CONF_FILE;
|
||||
if file_is_readable(test.as_str()) {
|
||||
return Some(test);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
|
@ -473,41 +560,40 @@ pub fn load_prefs() -> Preferences {
|
|||
match toml {
|
||||
Ok(toml) => (toml, Some(path)),
|
||||
Err(error) => {
|
||||
error!("Failed to open preferences file '{path}': {error}");
|
||||
eprintln!("Error: Failed to open preferences file '{path}': {error}");
|
||||
return Preferences::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Found no preference file, using default preferences.");
|
||||
(include_str!("data/outfly.toml").to_string(), None)
|
||||
println!("Found no preference file, using default preferences.");
|
||||
(DEFAULT_CONFIG_TOML.to_string(), None)
|
||||
}
|
||||
};
|
||||
match toml.parse::<DocumentMut>() {
|
||||
Ok(doc) => match toml_edit::de::from_document::<Preferences>(doc) {
|
||||
Ok(mut pref) => {
|
||||
if let Some(path) = &path {
|
||||
info!("Loaded preference file from {path}");
|
||||
println!("Loaded preference file from {path}");
|
||||
} else {
|
||||
info!("Loaded preferences from internal defaults");
|
||||
println!("Loaded preferences from internal defaults");
|
||||
}
|
||||
pref.source_file = path;
|
||||
dbg!(&pref);
|
||||
return pref;
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Failed to read preference line: {error}");
|
||||
eprintln!("Error: Failed to read preference line: {error}");
|
||||
return Preferences::default();
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Failed to open preferences: {error}");
|
||||
eprintln!("Error: Failed to open preferences: {error}");
|
||||
return Preferences::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct GameVars {
|
||||
pub db: HashMap<String, String>,
|
||||
}
|
||||
|
@ -556,15 +642,7 @@ impl GameVars {
|
|||
// and if a scope is missing, it prefixes the fallback scope.
|
||||
// Should NOT be used on non-variable values, like plain strings.
|
||||
//
|
||||
// Some examples, assuming fallback_scope="Clippy", SCOPE_SEPARATOR="$":
|
||||
//
|
||||
// "" -> "clippy$"
|
||||
// "foo" -> "clippy$foo"
|
||||
// "FOO" -> "clippy$foo"
|
||||
// "$foo" -> "clippy$foo"
|
||||
// "$$foo" -> "$$foo"
|
||||
// "PizzaClippy$foo" -> "pizzaclippy$foo" (unchanged)
|
||||
// "$foo$foo$foo$foo" -> "$foo$foo$foo$foo" (unchanged)
|
||||
// See test_normalize_varname() for examples.
|
||||
pub fn normalize_varname(fallback_scope: &str, key: &str) -> String {
|
||||
let parts: Vec<&str> = key.split(SCOPE_SEPARATOR).collect();
|
||||
let key: String = if parts.len() == 1 {
|
||||
|
@ -573,7 +651,7 @@ impl GameVars {
|
|||
} else if parts.len() > 1 {
|
||||
// we got a key with at least one "$"
|
||||
// extract anything before the last "$":
|
||||
let scope_part: String = parts[0..parts.len() - 2].join(SCOPE_SEPARATOR);
|
||||
let scope_part: String = parts[0..parts.len() - 1].join(SCOPE_SEPARATOR);
|
||||
|
||||
if scope_part.is_empty() {
|
||||
// we got a key like "$foo", just prefix the fallback scope
|
||||
|
@ -621,31 +699,51 @@ impl GameVars {
|
|||
// Check whether the two are identical.
|
||||
let mut left: String = parts[0].to_string();
|
||||
if left.contains(SCOPE_SEPARATOR) {
|
||||
left = self
|
||||
.get(Self::normalize_varname(scope, left.as_str()).as_str())
|
||||
.unwrap_or("".to_string());
|
||||
let key = Self::normalize_varname(scope, left.as_str());
|
||||
let value = self.get(key.as_str());
|
||||
left = if let Some(value) = value {
|
||||
value
|
||||
} else {
|
||||
warn!("Couldn't find variable `{key}` on left hand side of a condition");
|
||||
"".to_string()
|
||||
};
|
||||
}
|
||||
let mut right: String = parts[1].to_string();
|
||||
if right.contains(SCOPE_SEPARATOR) {
|
||||
right = self
|
||||
.get(Self::normalize_varname(scope, right.as_str()).as_str())
|
||||
.unwrap_or("".to_string());
|
||||
let key = Self::normalize_varname(scope, right.as_str());
|
||||
let value = self.get(key.as_str());
|
||||
right = if let Some(value) = value {
|
||||
value
|
||||
} else {
|
||||
warn!("Couldn't find variable `{key}` on right hand side of a condition");
|
||||
"".to_string()
|
||||
};
|
||||
}
|
||||
return left == right;
|
||||
} else {
|
||||
// Got something like "if $something != somethingelse bla bla"
|
||||
let mut left: String = parts[0].to_string();
|
||||
if left.contains(SCOPE_SEPARATOR) {
|
||||
left = self
|
||||
.get(Self::normalize_varname(scope, left.as_str()).as_str())
|
||||
.unwrap_or("".to_string());
|
||||
let key = Self::normalize_varname(scope, left.as_str());
|
||||
let value = self.get(key.as_str());
|
||||
left = if let Some(value) = value {
|
||||
value
|
||||
} else {
|
||||
warn!("Couldn't find variable `{key}` on left hand side of a condition");
|
||||
"".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
let mut right: String = parts[2..parts.len()].join(" ").to_string();
|
||||
if right.contains(SCOPE_SEPARATOR) {
|
||||
right = self
|
||||
.get(Self::normalize_varname(scope, right.as_str()).as_str())
|
||||
.unwrap_or("".to_string());
|
||||
let key = Self::normalize_varname(scope, right.as_str());
|
||||
let value = self.get(key.as_str());
|
||||
right = if let Some(value) = value {
|
||||
value
|
||||
} else {
|
||||
warn!("Couldn't find variable `{key}` on right hand side of a condition");
|
||||
"".to_string()
|
||||
};
|
||||
}
|
||||
let floats = (left.parse::<f64>(), right.parse::<f64>());
|
||||
let operator: &str = parts[1];
|
||||
|
@ -696,6 +794,23 @@ impl GameVars {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_varname() {
|
||||
assert_eq!(GameVars::normalize_varname("Clippy", ""), "clippy$");
|
||||
assert_eq!(GameVars::normalize_varname("Clippy", "foo"), "clippy$foo");
|
||||
assert_eq!(GameVars::normalize_varname("Clippy", "FOO"), "clippy$foo");
|
||||
assert_eq!(GameVars::normalize_varname("Clippy", "$foo"), "clippy$foo");
|
||||
assert_eq!(GameVars::normalize_varname("Clippy", "$$foo"), "$$foo");
|
||||
assert_eq!(
|
||||
GameVars::normalize_varname("Clippy", "PizzaClippy$foo"),
|
||||
"pizzaclippy$foo"
|
||||
);
|
||||
assert_eq!(
|
||||
GameVars::normalize_varname("Clippy", "$foo$foo$foo$foo"),
|
||||
"$foo$foo$foo$foo"
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct CommandLineOptions {
|
||||
pub window_mode_fullscreen: WindowMode,
|
||||
|
|
13
src/world.rs
13
src/world.rs
|
@ -19,6 +19,7 @@ use bevy_xpbd_3d::prelude::*;
|
|||
use fastrand;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const ENABLE_ASTEROIDS: bool = false;
|
||||
const ASTEROID_UPDATE_INTERVAL: f32 = 0.1; // seconds
|
||||
const ASTEROID_SIZE_FACTOR: f32 = 10.0;
|
||||
const RING_THICKNESS: f64 = 8.0e6;
|
||||
|
@ -36,19 +37,21 @@ pub struct WorldPlugin;
|
|||
impl Plugin for WorldPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup);
|
||||
app.add_systems(PostUpdate, handle_despawn);
|
||||
app.add_systems(Update, spawn_despawn_asteroids);
|
||||
app.add_systems(Update, handle_respawn.run_if(on_event::<RespawnEvent>()));
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
//app.add_plugins(PhysicsDebugPlugin::default());
|
||||
app.insert_resource(Gravity(DVec3::splat(0.0)));
|
||||
app.insert_resource(ActiveAsteroids(HashMap::new()));
|
||||
app.add_event::<RespawnEvent>();
|
||||
if ENABLE_ASTEROIDS {
|
||||
app.insert_resource(AsteroidUpdateTimer(Timer::from_seconds(
|
||||
ASTEROID_UPDATE_INTERVAL,
|
||||
TimerMode::Repeating,
|
||||
)));
|
||||
app.insert_resource(ActiveAsteroids(HashMap::new()));
|
||||
app.add_systems(Update, spawn_despawn_asteroids);
|
||||
app.add_systems(PostUpdate, handle_despawn_asteroids);
|
||||
app.add_event::<DespawnAsteroidEvent>();
|
||||
app.add_event::<RespawnEvent>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,7 +347,7 @@ fn spawn_despawn_asteroids(
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_despawn(
|
||||
fn handle_despawn_asteroids(
|
||||
mut commands: Commands,
|
||||
mut er_despawn: EventReader<DespawnAsteroidEvent>,
|
||||
mut db: ResMut<ActiveAsteroids>,
|
||||
|
|
Loading…
Reference in a new issue