Compare commits

...

68 commits

Author SHA1 Message Date
yuni 5ae8c7dc25 Sus now tries to kill you if you offend him 2024-06-15 02:13:33 +02:00
yuni 982ea00fc7 more dialogue for Sus 2024-06-15 01:50:43 +02:00
yuni ef319766fc update changelog 2024-06-15 01:41:42 +02:00
yuni a01bf79542 move Sus to pizzeria 2024-06-14 17:09:52 +02:00
yuni 8ce084b72f require less oxygen for emergency conversation option 2024-06-14 17:09:24 +02:00
yuni cd98b16926 add conversation for Sus 2024-06-14 17:00:13 +02:00
yuni 02499e8d05 add "sus" suit 2024-06-14 15:51:31 +02:00
yuni c273217f65 implement noise cancellation modes 2024-06-13 04:06:15 +02:00
yuni 4e8794338a fix wrong radio playing on startup 2024-06-13 03:41:15 +02:00
yuni 8c7a856717 implement radio stations 2024-06-13 03:26:19 +02:00
yuni f1512e01c9 create config file/directory if it doesn't exist yet 2024-06-13 02:18:58 +02:00
yuni eedc379c8d save/load many of the game settings to/from config file 2024-06-13 02:18:22 +02:00
yuni 2eb68e94f7 cleanup 2024-06-13 00:43:01 +02:00
yuni 43756fc09c platform-independent configuration file path 2024-06-13 00:42:23 +02:00
yuni 0f0d1aa1a9 cleanup 2024-06-13 00:39:11 +02:00
yuni 4ed006c548 fix console output in var::load_prefs (bevy macros dont work here) 2024-06-13 00:05:45 +02:00
yuni e579fdcdb7 some brainstorming on potential characters 2024-06-12 04:28:50 +02:00
yuni 4fa486946e move ROADMAP.md to doc/gamedesign.md 2024-06-12 02:43:24 +02:00
yuni e6c9ee9f3f update changelog 2024-06-12 02:41:32 +02:00
yuni 5082449c11 add option to ask Chef for oxygen 2024-06-12 02:35:36 +02:00
yuni a3c87e1651 fix multiple includes per chat
previously, putting everything into a "changeset" first (where each item
says "replace line X with the following block") and applying the
changeset after collecting all changes would lead to wrong line numbers,
since inserting a block will move all lines down by some amount.

Gotta replace the include-line with the block right away before you move
on to following include lines.

This was so complex in the first place to please rust's borrow checker.
2024-06-12 02:29:11 +02:00
yuni f8913b8fa3 fix positioning of scene objects 2024-06-12 01:30:07 +02:00
yuni 94ea7ecec2 fix conversation, jupiter is not always a crescent anymore 2024-06-12 01:26:04 +02:00
yuni 727cdcb0c1 better conversation flow for Chef 2024-06-12 01:23:31 +02:00
yuni 009a7ba1dd you can now ask Icarus for oxygen if you're suffocating 2024-06-12 01:06:11 +02:00
yuni 9e121cf633 fix off-by-one error in GameVars::normalize_varname() 2024-06-12 00:59:07 +02:00
yuni 0c622f28ab add debugging code to GameVar::evaluate_condition() 2024-06-12 00:58:38 +02:00
yuni 625bf21c84 continuously set/update chat variable $$player_oxygen_seconds 2024-06-12 00:57:55 +02:00
yuni 3b540b290d debug key now dumps all game variables to console 2024-06-12 00:57:42 +02:00
yuni 8636b08b5f add unit test for GameVars::normalize_varname() 2024-06-12 00:54:26 +02:00
yuni 7ec52b3503 more icarus dialogue 2024-06-11 23:11:28 +02:00
yuni 83f3f03aff update changelog 2024-06-11 06:01:52 +02:00
yuni 9d54a9d412 add scene's coordinates to scene object's coordinates 2024-06-11 05:59:10 +02:00
yuni f2246a247f implement scene loader (transformations are still a bit wonky) 2024-06-11 05:50:15 +02:00
yuni a3661cc43f add cruiser template 2024-06-11 05:50:13 +02:00
yuni 2c1dacbf03 flesh out scene loader 2024-06-11 05:49:59 +02:00
yuni 0047c4eda4 WIP scene definitions from blender files 2024-06-11 04:40:57 +02:00
yuni 28cb1c09fd add tutorial link 2024-06-11 04:10:15 +02:00
yuni beaf8cff47 brevity 2024-06-11 04:09:16 +02:00
yuni ee818beea4 wording 2024-06-11 04:07:20 +02:00
yuni 8c97a962b2 add gravity/orbit changes to changelog 2024-06-11 03:59:04 +02:00
yuni 1a94c31d62 Merge branch 'gravity' 2024-06-11 03:40:21 +02:00
yuni 6275a64d7c ignore gravity when calculating g forces
Because gravity is obviously not a force, just a magical property of
spacetime, conveyed by messenger particles called Higgs Bosons which act
as force carriers for this non-force.  Who made this up? :D
2024-06-11 03:29:50 +02:00
yuni 91bf2ddc54 fix velocity after using stop cheat 2024-06-11 03:04:54 +02:00
yuni 79351dc4d0 fix velocity after using teleport cheats 2024-06-11 02:59:10 +02:00
yuni 4dd195e17a fix velocity after traveling with busses 2024-06-11 02:54:13 +02:00
yuni 3d26b0915d add Id2V resource, "wants matchvelocitywith" command 2024-06-11 02:46:31 +02:00
yuni 46a030f15e speedometer shows speed relative to orbital velocity 2024-06-11 01:12:41 +02:00
yuni d04b400fad "space" key now slows down relative to orbital velocity 2024-06-11 01:04:23 +02:00
yuni 77c1bd1e6a add game::JupiterPos resource 2024-06-11 00:58:56 +02:00
yuni 5817944a79 add a line about darkness to the tutorial 2024-06-11 00:50:56 +02:00
yuni cd13b529c3 base handle_wants_maxvelocity on current orbital velocity 2024-06-11 00:50:25 +02:00
yuni 76272a7fc2 add "target_velocity" var to handle_wants_maxvelocity() 2024-06-11 00:05:03 +02:00
yuni d9af542d54 give objects orbiting jupiter an orbital velocity on startup 2024-06-10 23:19:33 +02:00
yuni e16a650b22 apply gravity towards Jupiter for objects orbiting Jupiter 2024-06-10 23:19:33 +02:00
yuni 7be6b0746f disable ring asteroids by default 2024-06-10 23:19:33 +02:00
yuni 974bf9cb8d add feature flag for toggling the generic ring asteroids 2024-06-10 23:19:33 +02:00
yuni e56f931951 ignore gforce for 0.01s on startup, to survive a large starting velocity 2024-06-10 23:19:33 +02:00
yuni 24a9b208bd fix direction of orbital velocity 2024-06-10 23:19:33 +02:00
yuni 1614ece72a update the 2 new nature functions using DVec3 instead of Vec3 2024-06-10 23:19:33 +02:00
yuni 169b9ee257 add nature::gravitational_acceleration, incl. test 2024-06-10 23:19:33 +02:00
yuni c6750eae46 add nature::orbital_velocity 2024-06-10 23:19:33 +02:00
yuni 159dfe8e19 add comment 2024-06-10 23:19:33 +02:00
yuni 797b106255 lower player closer to the orbital plane 2024-06-10 23:19:33 +02:00
yuni 2f82a27ab2 change player's orbital position based on game start time 2024-06-10 23:19:33 +02:00
yuni d3fb7422bf add a tutorial 2024-06-10 23:02:19 +02:00
yuni 1433773784 add suffocation sound effects 2024-06-07 23:55:04 +02:00
yuni 727d28089f don't reset third person setting on death 2024-06-07 03:52:24 +02:00
28 changed files with 1528 additions and 483 deletions

View file

@ -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
View file

@ -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"

View file

@ -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"]

View file

@ -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

Binary file not shown.

BIN
assets/sounds/gasp.ogg Normal file

Binary file not shown.

Binary file not shown.

View file

@ -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;
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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();

Binary file not shown.

BIN
src/blender/sus.blend Normal file

Binary file not shown.

View file

@ -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 {

View file

@ -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(),
);
}
}

View file

@ -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

View file

@ -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(

View file

@ -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"

View file

@ -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
View 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]),
]

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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",

View file

@ -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);

View file

@ -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 => {

View file

@ -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(),

View file

@ -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,

View file

@ -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>,