Compare commits

...

33 Commits

Author SHA1 Message Date
hut 86ce38eda1 experiment: mute reaction wheels, unless inside vehicle 2024-04-11 21:42:42 +02:00
hut 629f6ac900 bump to v0.6.1 2024-04-11 21:41:34 +02:00
hut b2e6ba2b77 reset velocity when waking up after bus station cryo 2024-04-11 21:30:27 +02:00
hut ef0ada9dac reset *some* settings on death 2024-04-11 21:20:54 +02:00
hut 053f2827dd handle god mode properly when restarting/cheat-dying 2024-04-11 21:17:34 +02:00
hut cb698cc3b2 Revert "reset settings on death/restart"
This reverts commit 3676fd444c.
2024-04-11 21:12:56 +02:00
hut 3676fd444c reset settings on death/restart 2024-04-11 21:07:01 +02:00
hut d0df5c5dbd implement rotation stabilizer (key Y) 2024-04-11 21:06:28 +02:00
hut 9ea79ff80f death animation fallback 2024-04-11 20:58:16 +02:00
hut 0c31055422 add key binding F7 to restart the game 2024-04-11 20:47:11 +02:00
hut eb481edc8e implement DamageTypes, different visuals/sounds on death 2024-04-11 20:46:52 +02:00
hut 2b74b50563 despawn AR overlay entities on death 2024-04-11 20:06:00 +02:00
hut 0d312edeee fix bringing space crafts along with a bus ride 2024-04-11 02:14:36 +02:00
hut dc1037e5a3 implement trips back to serenity station 2024-04-11 02:10:57 +02:00
hut a3ea057994 implement trips to metis prime station and serenity station 2024-04-11 01:55:32 +02:00
hut 9954c19d2a implement travel to Oscillation Station 2024-04-11 01:34:16 +02:00
hut a6f6b8b582 implement bus stops (no riding yet) 2024-04-11 01:12:07 +02:00
hut db3545e9a3 normalize mouse sensitivity across screen resolutions 2024-04-10 22:51:11 +02:00
hut abaed74424 update second MeteorAceGT 2024-04-10 22:50:59 +02:00
hut 5f56d63d32 Space Pizza™ chef: add chef hat AR overlay 2024-04-10 22:36:19 +02:00
hut 86c2c5e410 typo² 2024-04-10 22:10:05 +02:00
hut cd1f8c18cf move from mass-based to density-based definitions 2024-04-10 22:05:28 +02:00
hut 860d7f8d4b clamp fov change with g force 2024-04-10 22:04:07 +02:00
hut 8b2debfa37 space suit: normalize scale 2024-04-10 21:26:20 +02:00
hut d064680d60 clippy: add AR face ^_^ 2024-04-10 21:03:30 +02:00
hut 817a5e2a96 add developer commentary 2024-04-10 19:29:33 +02:00
hut 10d65f8c02 clippy: brighter thrusters 2024-04-10 18:28:28 +02:00
hut 96b8ed22b2 clippy: balance, center 2024-04-10 18:26:49 +02:00
hut e4c3eccb02 typo 2024-04-10 17:54:46 +02:00
hut b3d63301cc clippy: chat 2024-04-10 17:54:43 +02:00
hut c7b4216c57 clippy: shorter name 2024-04-10 17:52:38 +02:00
hut 8c51e67228 add outfly_*.zip to .gitignore 2024-04-10 17:49:29 +02:00
hut b8cabe7ac1 add Clippy™ Convenience Drone 2024-04-10 17:48:07 +02:00
20 changed files with 538 additions and 77 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ assets/tmp
assets/external
*.blend1
extra
outfly_*.zip

20
COMMENTARY.md Normal file
View File

@ -0,0 +1,20 @@
# Developer Commentary
## Clippy Convenience Companion
Clippy™ Convenience Companion is a self-assembling, self-replicating, highly modular being that emerges from the collection of individual Clippy™ cubes.
These cubes are interchangeable, solar panel plated, driven by strong reaction wheels, and connect to each other through powerful magnets along the surface. A sensor array picks up various EM frequencies and a camera image through the plating. There are no moving parts on the outside. Individual cubes are able to transfer electricity to connected cubes, as well as transmit information wirelessly.
The shape of a Clippy™ can change quickly to solve a variety of space problems. Off-the-shelf plug-and-play Clippy™ modules further enhance their agency with additional tools, such as propulsion modules, sensors, or manipulators.
A Clippy™ Convenience Companion will broadcast an Augmented Reality overlay that resembles a face, in the somewhat antiquated "kaomoji" style of the late 1900's, typically a friendly face to instill trust and kindness in viewers. How a unified persona with aligned intentions emerges from the interactions of the individual Clippy™ cubes is, as of writing, still subject to debate.
Clippy™ is inspired by [self-assembling cube robots](https://www.youtube.com/watch?v=hI5UDKaWJOo), by the [paperclip maximizer thought experiment](https://en.wikipedia.org/w/index.php?title=Instrumental_convergence&oldid=1210129297#Paperclip_maximizer) (which is also the source of the name), by [Star Gate's Replicators](https://stargate.fandom.com/wiki/Replicator), and finally, by [Fallout New Vegas' "Yes Man" robot](https://fallout.fandom.com/wiki/Yes_Man).
## MeteorAceGT™ Sports Racing Capsule
This icosahedral sports spacecraft is designed for maximum stability and maneuverability at high velocities, not only in vacuum but also in thick atmospheres. Strong reaction wheels allow perfect control of spacecraft rotation, and allow you to land the spacecraft safely on any planet or moon. Powerful nuclear-powered electromagnets shield from ionized particle radiation, allowing you to surf the clouds of Jupiter safely, with the spherical shape providing maximum structural integrity.
The excessive forward thrust will get many pilots to pass out, while using close to zero propellant thanks to the advanced ion engine array. No dedicated backward thrusters are available for structural integrity in atmospheres. This is compensated by the sideways thrusters, which are mildly inclined to the forward direction, providing a small amount of backward thrust.
The design of this racing capsule was inspired [Shotaro Kaneda's motorbike from Akira](https://akira.fandom.com/wiki/Shotaro_Kaneda%27s_Bike) and by the [orbital module of the Soyuz spacecraft](https://en.wikipedia.org/w/index.php?title=Soyuz_%28spacecraft%29&oldid=1218141107#Orbital_module).

2
Cargo.lock generated
View File

@ -2769,7 +2769,7 @@ dependencies = [
[[package]]
name = "outfly"
version = "0.6.0"
version = "0.6.1"
dependencies = [
"bevy",
"bevy_embedded_assets",

View File

@ -1,6 +1,6 @@
[package]
name = "outfly"
version = "0.6.0"
version = "0.6.1"
edition = "2021"
homepage = "https://codeberg.org/hut/outfly"
repository = "https://codeberg.org/hut/outfly"

View File

@ -32,6 +32,7 @@ Links:
- R: Rotate (hold & move mouse)
- E: Interact: Talk to people, enter vehicles
- Q: Exit vehicle
- F7: Restart game
- JKULIO: Mouseless camera rotation
- Augmented Reality: (toggle with Tab)
- Left click: Target objects
@ -40,6 +41,7 @@ Links:
- Tab: Toggle HUD/AR
- F11: Toggle fullscreen
- F: Toggle 3rd person view
- Y: Toggle rotation stabilizer
- T: Toggle music
- M: Toggle sound effects
- Cheats
@ -151,6 +153,10 @@ python -m http.server -d wasm
# Changelog
- v0.6.1:
- Implement free public transport with 3 bus stations
- Implement Clippy™ Convenience Companion drone
- Implement augmented reality overlays on top of NPC appearances
- v0.6.0:
- Implement zooming with right click (AR only)
- Implement targeting objects with left click (AR only)

BIN
assets/models/clippy.glb Normal file

Binary file not shown.

BIN
assets/models/clippy_ar.glb Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -33,7 +33,20 @@ impl Plugin for ActorPlugin {
}
}
#[derive(Event)] pub struct PlayerDiesEvent;
#[derive(Copy, Clone)]
pub enum DamageType {
Unknown,
Mental,
Trauma,
Asphyxiation,
//Poison,
//Radiation,
//Freeze,
//Burn,
}
#[derive(Event)] pub struct PlayerDiesEvent(pub DamageType);
#[derive(Event)]
pub struct VehicleEnterExitEvent {
vehicle: Entity,
@ -63,6 +76,7 @@ pub struct HitPoints {
pub current: f32,
pub max: f32,
pub damage: f32,
pub damagetype: DamageType,
}
impl Default for HitPoints {
fn default() -> Self {
@ -70,6 +84,7 @@ impl Default for HitPoints {
current: 100.0,
max: 100.0,
damage: 0.0,
damagetype: DamageType::Unknown,
}
}
}
@ -211,6 +226,7 @@ pub fn update_physics_lifeforms(
suit.oxygen = (suit.oxygen - oxygen_drain*d).clamp(0.0, suit.oxygen_max);
if suit.oxygen <= 0.0 {
hp.damage += 1.0 * d;
hp.damagetype = DamageType::Asphyxiation;
}
}
}
@ -218,13 +234,14 @@ pub fn update_physics_lifeforms(
pub fn handle_input(
mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>,
settings: ResMut<settings::Settings>,
mut settings: ResMut<settings::Settings>,
q_talker: Query<(&chat::Talker, &Transform), (Without<actor::Player>, Without<Camera>)>,
player: Query<Entity, With<actor::Player>>,
q_camera: Query<&Transform, With<Camera>>,
q_vehicles: Query<(Entity, &Transform), (With<actor::Vehicle>, Without<actor::Player>, Without<Camera>)>,
mut ew_conv: EventWriter<chat::StartConversationEvent>,
mut ew_vehicle: EventWriter<VehicleEnterExitEvent>,
mut ew_playerdies: EventWriter<PlayerDiesEvent>,
q_player_drives: Query<Entity, With<PlayerDrivesThis>>,
) {
if q_camera.is_empty() || player.is_empty() {
@ -278,6 +295,10 @@ pub fn handle_input(
break;
}
}
else if keyboard_input.just_pressed(settings.key_restart) {
settings.god_mode = false;
ew_playerdies.send(PlayerDiesEvent(DamageType::Mental));
}
}
pub fn handle_vehicle_enter_exit(
@ -403,12 +424,13 @@ fn handle_player_death(
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_effect: EventWriter<effects::SpawnEffectEvent>,
mut log: ResMut<hud::Log>,
settings: Res<settings::Settings>,
mut settings: ResMut<settings::Settings>,
) {
for _ in er_playerdies.read() {
for death in er_playerdies.read() {
if settings.god_mode {
return;
}
settings.reset_player_settings();
for entity in &q_noscenes {
cmd.entity(entity).despawn();
}
@ -418,11 +440,30 @@ fn handle_player_death(
}
log.clear();
//cmd.run_system(commands::load_defs); // why is it so complicated to get SystemId?
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::WakeUp));
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeIn(Color::MAROON),
duration: 1.0,
});
match death.0 {
DamageType::Mental => {
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeIn(Color::BLACK),
duration: 4.0,
});
}
DamageType::Asphyxiation => {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::WakeUp));
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeIn(Color::BLACK),
duration: 1.0,
});
}
DamageType::Trauma | _ => {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::WakeUp));
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeIn(Color::MAROON),
duration: 1.0,
});
}
}
commands::load_defs(ew_spawn);
return;
}
@ -439,7 +480,7 @@ fn handle_damage(
hp.current -= hp.damage;
}
if hp.current <= 0.0 {
ew_playerdies.send(PlayerDiesEvent);
ew_playerdies.send(PlayerDiesEvent(hp.damagetype));
}
}
else {
@ -464,6 +505,7 @@ fn handle_gforce(
}
if gforce.gforce > gforce.damage_threshold {
hp.damage += (gforce.gforce - gforce.damage_threshold).powf(2.0) / 3000.0;
hp.damagetype = DamageType::Trauma;
}
if gforce.visual_effect > 0.0001 {

View File

@ -105,7 +105,7 @@ pub fn update_fov(
settings.is_zooming = true;
}
} else {
fov = (gforce.visual_effect * settings.fov_highspeed + settings.fov).to_radians();
fov = (gforce.visual_effect.clamp(0.0, 1.0) * settings.fov_highspeed + settings.fov).to_radians();
if settings.is_zooming {
settings.is_zooming = false;
}
@ -117,10 +117,15 @@ pub fn update_fov(
pub fn handle_input(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<settings::Settings>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
) {
if keyboard_input.just_pressed(settings.key_camera) {
settings.third_person ^= true;
}
if keyboard_input.just_pressed(settings.key_rotation_stabilizer) {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
settings.rotation_stabilizer_active ^= true;
}
}
fn manage_player_actor(
@ -157,7 +162,7 @@ fn manage_player_actor(
pub fn apply_input_to_player(
time: Res<Time>,
settings: Res<settings::Settings>,
mut windows: Query<&mut Window, With<PrimaryWindow>>,
windows: Query<&Window, With<PrimaryWindow>>,
mut mouse_events: EventReader<MouseMotion>,
key_input: Res<ButtonInput<KeyCode>>,
thruster_sound_controller: Query<&AudioSink, With<audio::ComponentThrusterSound>>,
@ -171,16 +176,23 @@ pub fn apply_input_to_player(
&mut AngularVelocity,
&mut LinearVelocity,
&mut ExternalTorque,
Option<&actor::PlayerDrivesThis>,
), (With<actor::PlayerCamera>, Without<Camera>)>,
) {
let dt = time.delta_seconds();
let mut play_thruster_sound = false;
let mut axis_input: DVec3 = DVec3::ZERO;
let window_result = windows.get_single_mut();
let (win_res_x, win_res_y): (f32, f32);
let mut focused = true;
if window_result.is_ok() {
focused = window_result.unwrap().focused;
if let Ok(window) = &windows.get_single() {
focused = window.focused;
win_res_x = window.resolution.width();
win_res_y = window.resolution.height();
}
else {
win_res_x = 1920.0;
win_res_y = 1050.0;
}
let target_v: DVec3 = if let Ok(target) = q_target.get_single() {
@ -189,7 +201,7 @@ pub fn apply_input_to_player(
DVec3::splat(0.0)
};
if let Ok((player_transform, mut engine, mut angularvelocity, mut v, mut torque)) = q_playercam.get_single_mut() {
if let Ok((player_transform, mut engine, mut angularvelocity, mut v, mut torque, bike)) = q_playercam.get_single_mut() {
// Handle key input
if focused {
if key_input.pressed(settings.key_forward) {
@ -288,20 +300,24 @@ pub fn apply_input_to_player(
}
if mouse_delta != Vec2::ZERO {
if key_input.pressed(settings.key_rotate) {
pitch_yaw_rot[2] += mouse_delta.x;
pitch_yaw_rot[2] += 1000.0 * mouse_delta.x / win_res_x;
} else {
pitch_yaw_rot[0] += mouse_delta.y;
pitch_yaw_rot[1] -= mouse_delta.x;
pitch_yaw_rot[0] += 1000.0 * mouse_delta.y / win_res_y;
pitch_yaw_rot[1] -= 1000.0 * mouse_delta.x / win_res_x;
}
}
let angular_slowdown: f64 = (2.0 - engine.reaction_wheels.powf(0.01).clamp(1.001, 1.1)) as f64;
let angular_slowdown: f64 = if settings.rotation_stabilizer_active {
(2.0 - engine.reaction_wheels.powf(0.01).clamp(1.001, 1.1)) as f64
} else {
1.0
};
if pitch_yaw_rot.length_squared() > 1.0e-18 {
play_reactionwheel_sound = true;
pitch_yaw_rot *= settings.mouse_sensitivity * sensitivity_factor * engine.reaction_wheels;
torque.apply_torque(DVec3::from(
player_transform.rotation * Vec3::new(
pitch_yaw_rot[0] * 2.0,
pitch_yaw_rot[0],
pitch_yaw_rot[1],
pitch_yaw_rot[2])));
angularvelocity.0 *= angular_slowdown.clamp(0.97, 1.0) as f64;
@ -320,7 +336,7 @@ pub fn apply_input_to_player(
let volume = sink.volume();
let speed = sink.speed();
let action = pitch_yaw_rot.length_squared().powf(0.2) * 0.0005;
if play_reactionwheel_sound && !settings.mute_sfx {
if play_reactionwheel_sound && !settings.mute_sfx && bike.is_some() {
sink.set_volume((volume + action).clamp(0.0, 1.0));
sink.set_speed((speed + action * 0.2).clamp(0.2, 0.5));
sink.play()

View File

@ -1,5 +1,7 @@
use bevy::prelude::*;
use crate::{actor, audio, hud, settings, world};
use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3;
use crate::{actor, audio, hud, settings, world, effects};
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
@ -300,12 +302,15 @@ pub fn handle_conversations(
pub fn handle_chat_scripts(
mut er_chatscript: EventReader<ChatScriptEvent>,
mut q_actor: Query<(&mut actor::Actor, &mut actor::Suit), Without<actor::Player>>,
mut q_player: Query<(&mut actor::Actor, &mut actor::Suit), With<actor::Player>>,
mut q_player: Query<(&mut actor::Actor, &mut actor::Suit, &mut actor::ExperiencesGForce), With<actor::Player>>,
mut q_playercam: Query<(&mut Position, &mut LinearVelocity), With<actor::PlayerCamera>>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_effect: EventWriter<effects::SpawnEffectEvent>,
) {
for script in er_chatscript.read() {
match script.name.as_str() {
"refilloxygen" => if let Ok(mut amount) = script.param.parse::<f32>() {
for (mut _actor, mut suit) in q_player.iter_mut() {
for (_, mut suit, _) in q_player.iter_mut() {
if script.param2.is_empty() {
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
}
@ -334,7 +339,49 @@ pub fn handle_chat_scripts(
} else {
error!("Invalid parameter for command `{}`: `{}`", script.name, script.param);
}
_ => {}
"cryotrip" => {
if script.param.is_empty() {
error!("Chat script cryotrip needs a parameter");
}
else {
if let Ok((mut pos, mut v)) = q_playercam.get_single_mut() {
if script.param == "oscillation".to_string() {
*pos = Position(DVec3::new(147e6, 165e6, 336e6));
v.0 = DVec3::ZERO;
}
else if script.param == "metisprime".to_string() {
*pos = Position(DVec3::new(27643e3, -47e3, -124434e3));
v.0 = DVec3::ZERO;
}
else if script.param == "serenity".to_string() {
*pos = Position(DVec3::new(-121095e3, 582e3, -190816e3));
v.0 = DVec3::ZERO;
}
else {
error!("Invalid destination for cryotrip chat script: '{}'", script.param);
}
}
if let Ok((_, mut suit, mut gforce)) = q_player.get_single_mut() {
suit.oxygen = suit.oxygen_max;
gforce.ignore_gforce_seconds = 1.0;
}
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::WakeUp));
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeIn(Color::CYAN),
duration: 1.0,
});
}
}
"cryofadeout" => {
ew_effect.send(effects::SpawnEffectEvent {
class: effects::Effects::FadeOut(Color::CYAN),
duration: 5.1,
});
}
_ => {
let script_name = &script.name;
error!("Error, undefined chat script {script_name}");
}
}
}
}

View File

@ -61,12 +61,13 @@ struct ParserState {
warmup_seconds: f32,
engine_type: actor::EngineType,
oxygen: f32,
mass: f64,
density: f64,
collider: Collider,
camdistance: f32,
suit_integrity: f32,
light_brightness: f32,
light_color: Option<Color>,
ar_model: Option<String>,
// Chat fields
delay: f64,
@ -79,6 +80,7 @@ struct ParserState {
script: String,
script_parameter: String,
script_parameter2: String,
sound: Option<String>,
}
impl Default for ParserState {
fn default() -> Self {
@ -115,12 +117,13 @@ impl Default for ParserState {
warmup_seconds: default_engine.warmup_seconds,
engine_type: default_engine.engine_type,
oxygen: nature::OXY_D,
mass: 1.0,
density: 100.0,
collider: Collider::sphere(1.0),
camdistance: default_actor.camdistance,
suit_integrity: 1.0,
light_brightness: 0.0,
light_color: None,
ar_model: None,
delay: 0.0,
text: "".to_string(),
@ -132,6 +135,7 @@ impl Default for ParserState {
script: "".to_string(),
script_parameter: "".to_string(),
script_parameter2: "".to_string(),
sound: Some("chat".to_string()),
}
}
}
@ -144,6 +148,10 @@ impl ParserState {
self.level = default.level;
self.text = default.text;
self.is_choice = default.is_choice;
self.script = default.script;
self.script_parameter = default.script_parameter;
self.script_parameter2 = default.script_parameter2;
self.sound = default.sound;
}
fn as_chatbranch(&self) -> chat::ChatBranch {
return chat::ChatBranch {
@ -151,7 +159,7 @@ impl ParserState {
name: self.name.clone().unwrap_or("".to_string()),
label: self.label.clone(),
delay: self.delay.clone(),
sound: if self.is_choice { "".to_string() } else { "chat".to_string() },
sound: if self.is_choice || self.sound.is_none() { "".to_string() } else { "chat".to_string() },
level: self.level.clone(),
reply: if self.is_choice { "".to_string() } else { self.text.clone() },
choice: if self.is_choice { self.text.clone() } else { "".to_string() },
@ -226,6 +234,8 @@ pub fn load_defs(
}
}
["relativeto", id] => {
// NOTE: call this command before "id", otherwise actors that
// set their position relative to this actor will get the wrong offset
match id2pos.get(&id.to_string()) {
Some(pos) => {
state.pos += *pos;
@ -371,9 +381,9 @@ pub fn load_defs(
continue;
}
}
["mass", value] => {
["density", value] => {
if let Ok(value_float) = value.parse::<f64>() {
state.mass = value_float;
state.density = value_float;
}
else {
error!("Can't parse float: {line}");
@ -453,6 +463,9 @@ pub fn load_defs(
continue;
}
}
["armodel", asset_name] => {
state.ar_model = Some(asset_name.to_string());
}
// Parsing chats
["chat", chat_name] => {
@ -537,6 +550,11 @@ pub fn load_defs(
debug!("Registering level: {}", level);
state.level = level.to_string();
}
["script", scriptname] => {
state.script = scriptname.to_string();
state.script_parameter = "".to_string();
state.script_parameter2 = "".to_string();
}
["script", scriptname, parameter] => {
state.script = scriptname.to_string();
state.script_parameter = parameter.to_string();
@ -547,6 +565,9 @@ pub fn load_defs(
state.script_parameter = parameter.to_string();
state.script_parameter2 = parameter2.to_string();
}
["sound", "none"] => {
state.sound = None;
}
_ => {
error!("No match for [{}]", parts.join(","));
}
@ -571,6 +592,8 @@ fn spawn_entities(
}
}
else if state.class == DefClass::Actor {
let actor_entity;
{
let mut actor = commands.spawn_empty();
actor.insert(actor::Actor {
id: state.id.clone(),
@ -614,11 +637,10 @@ fn spawn_entities(
// Physics Parameters
if state.has_physics {
let fix_scale: f64 = 1.0 / (state.model_scale as f64).powf(3.0);
actor.insert(RigidBody::Dynamic);
actor.insert(LinearVelocity(state.velocity));
actor.insert(AngularVelocity(state.angular_momentum));
actor.insert(ColliderDensity((state.mass * fix_scale) as f64));
actor.insert(ColliderDensity(state.density));
if state.collider_is_mesh {
actor.insert(AsyncSceneCollider::new(Some(
ComputedCollider::TriMesh
@ -699,6 +721,25 @@ fn spawn_entities(
..default()
});
}
if let Some(_) = state.ar_model {
actor.insert(hud::AugmentedRealityOverlayBroadcaster);
}
actor_entity = actor.id();
}
if let Some(ar_asset_name) = &state.ar_model {
commands.spawn((
hud::AugmentedRealityOverlay {
owner: actor_entity,
},
world::DespawnOnPlayerDeath,
SceneBundle {
scene: asset_server.load(world::asset_name_to_path(ar_asset_name)),
visibility: Visibility::Hidden,
..default()
},
));
}
}
}
}

View File

@ -13,13 +13,12 @@ actor 0 593051 0 suit
orbit 226000e3 0.66
player yes
id player
mass 200.0
scale 1
scale 2
oxygen 0.008
health 0.3
angularmomentum 0 0 0
collider capsule 2 1
thrust 1.2 1 1 300 1.5
collider capsule 1 0.5
thrust 1.2 1 1 400 1.5
rotationy 0.65
engine monopropellant
@ -28,11 +27,11 @@ actor 10 -30 20 MeteorAceGT
relativeto player
scale 5
vehicle yes
thrust 24.5 4.8 3.3 200000 3
thrust 24.5 4.8 3.3 500000 3
engine ion
collider sphere 1.5
collider sphere 1
camdistance 50
mass 3000
density 200
angularmomentum 0.1 0.1 0.3
actor 0 0 0 io
@ -85,14 +84,12 @@ actor 0 0 0 moonlet
id thebe
orbit 221900e3 0.66
scale 50e3
mass 430e15
angularmomentum 0 0.025 0
actor 3000 0 0 moonlet
name Moonlet
relativeto player
scale 500
mass 10000000
angularmomentum 0 0.15 0
actor -200 -110 1000 satellite
@ -105,13 +102,13 @@ actor -200 -110 1000 satellite
collider capsule 7.5 1
rotationy 0.5
angularmomentum 0 0 0
mass 10
density 0.01
actor 1000 20 300 monolith
name "Mysterious Monolith 1"
relativeto player
scale 2
mass 1000
density 300
rotationx 0.5
wants maxrotation 0.01
angularmomentum 0.0 0.0 0.01
@ -121,7 +118,7 @@ actor 10000 2000 -3500 monolith
name "Mysterious Monolith 2"
relativeto player
scale 2
mass 1000
density 300
rotationx 0.5
wants maxrotation 0.01
angularmomentum 0.0 0.0 0.01
@ -131,7 +128,7 @@ actor -8000 -1000 -100 monolith
name "Mysterious Monolith 3"
relativeto player
scale 2
mass 1000
density 300
rotationx 0.5
wants maxrotation 0.01
angularmomentum 0.0 0.0 0.01
@ -142,7 +139,6 @@ actor -3300 10 0 pizzeria
relativeto player
id pizzeria
scale 40
mass 1000000
rotationy 0.30
angularmomentum 0 0 0
actor -120 0 20 MeteorAceGT
@ -150,17 +146,17 @@ actor -3300 10 0 pizzeria
relativeto pizzeria
scale 5
vehicle yes
thrust 70 13.7 9.4 200000 20
thrust 24.5 4.8 3.3 500000 3
engine ion
collider sphere 1.5
collider sphere 1
camdistance 50
mass 3000
density 200
angularmomentum 0 0 0.2
actor -100 63 -13 pizzasign
name "Pizzeria Sign"
relativeto pizzeria
scale 20
mass 200
density 200
rotationy 0.45
angularmomentum 0 0 0
actor -16 -10 0 lightorb
@ -173,13 +169,29 @@ actor -3300 10 0 pizzeria
relativeto pizzeria
scale 0.5
light FF8F4A 1000000
actor -33 0 4 clippy
name "Clippy™ Convenience Companion"
relativeto pizzeria
armodel clippy_ar
angularmomentum 0 0 0
wants maxrotation 0
wants maxvelocity 0
thrust 15 6 3 400 0.5
rotationy -0.5
scale 3
chatid pizzaclippy
chat pizzaclippy
name "Clippy™ Convenience Companion"
msg 4 INIT EXIT "At your service!"
actor -35 0 0 suit
relativeto pizzeria
name "Space Pizza™"
chatid pizzeria
armodel suit_ar_chefhat
alive yes
mass 200.0
collider capsule 2 1
scale 2
collider capsule 1 0.5
thrust 1.2 1 1 10 1.5
wants maxrotation 0
wants maxvelocity 0
@ -193,7 +205,7 @@ actor -3300 10 0 pizzeria
msg 50 order special "Would you like to order today's special?"
choice 3 special whatsthespecial "What's the special?"
msg 4 whatsthespecial pineapple "Suspicious Spacefunghi"
msg 4 pineapple smoothie "With free pineapple imiation"
msg 4 pineapple smoothie "With free pineapple imitation"
msg 7 smoothie tube "Our pizza smoothies are freshly blended every day"
choice 3 tube wtftube "Wait... pizza smoothie?"
msg 6 wtftube anyway "Huh? Of course, smoothie! How else do you want to get that pizza down your spacesuit feeding tube?"
@ -236,8 +248,8 @@ actor 60 -15 -40 suit
name Icarus
chatid hi_icarus
alive yes
mass 200.0
collider capsule 2 1
scale 2
collider capsule 1 0.5
angularmomentum 0.4 0.2 0.1
rotationy 0.6
rotationx 1
@ -336,8 +348,8 @@ actor -300 0 40 suit
name "Drifter"
chatid drifter
oxygen 0.08
mass 200.0
collider capsule 2 1
scale 2
collider capsule 1 0.5
chat drifter
name "Drifter"
msg 2 INIT dead "Error: No response"
@ -349,6 +361,166 @@ actor -300 0 40 suit
script refilloxygen 1 drifter
msg 0 outcold EXIT ""
actor 100 -18000 2000 "orb_busstop"
relativeto player
id "busstop"
name "StarTrans Bus Stop: Serenity Station"
scale 100
wants maxrotation 0
wants maxvelocity 0
actor 120 864 150 clippy
relativeto "busstop"
id "busstopclippy"
name "StarTrans Clippy™"
armodel clippy_ar
angularmomentum 0 0 0
wants maxrotation 0
wants maxvelocity 0
thrust 15 6 3 400 0.5
rotationy -0.5
scale 3
chatid "busstopclippy"
chat "busstopclippy"
name "StarTrans Clippy™"
msg 2 INIT question "Welcome at StarTrans Cargo Services!"
msg 2 question wait "Ready for a trip? Available bus stops: Oscillation Station, Metis Prime Station"
msg 40 wait answer ""
sound none
choice 2 answer how1 "Bus stop?! How does this work?"
msg 6 how1 how2 "StarTrans Cargo Services is the most convenient way to travel the vast distances of space."
msg 6 how2 how3 "Just activate your suit's built-in cryostasis. A StarTrans carrier will pick you up and you will wake up at your destination in the blink of an eye."
msg 40 how3 answer "Of course we will supply you with free oxygen and ensure your safety."
choice 2 answer vehicle "Can I take a spacecraft with me?"
msg 40 vehicle answer "Absolutely."
choice 1 answer stopA1 "Take me to Oscillation Station, please."
msg 5 stopA1 stopA2 "StarTrans wishes you a pleasant journey."
script cryofadeout
msg 0 stopA2 EXIT ""
script cryotrip oscillation
sound none
choice 1 answer stopB1 "Take me to Metis Prime Station, please."
msg 5 stopB1 stopB2 "StarTrans wishes you a pleasant journey."
script cryofadeout
msg 0 stopB2 EXIT ""
script cryotrip metisprime
choice 1 answer stopC1 "Take me to Serenity Station, please."
msg 5 stopC1 stopC2 "StarTrans wishes you a pleasant journey."
script cryofadeout
msg 0 stopC2 EXIT ""
script cryotrip serenity
choice 1 answer oxy "Can you please fill up my oxygen tank without taking me anywhere?"
msg 2 oxy EXIT "Acceptable."
script refilloxygen 1000
sound none
choice 2 answer bye "No, thank you."
msg 2 bye EXIT "Feel free to come back any time."
msg 0 answer EXIT "Connection terminated."
actor 40 10 40 "orb_busstop"
name "Light Orb"
relativeto busstopclippy
light "47FF00" 1000000
actor 30 60 -10 "orb_busstop"
name "Light Orb"
relativeto busstopclippy
light "47FF00" 1000000
actor -10 -60 20 "orb_busstop"
name "Light Orb"
relativeto busstopclippy
light "47FF00" 1000000
actor -40 20 30 "orb_busstop"
name "Light Orb"
relativeto busstopclippy
light "47FF00" 1000000
actor 8 2 0 suit
relativeto "busstopclippy"
name "Rudy"
wants maxrotation 0.2
wants maxvelocity 0
thrust 1.2 1 1 400 1.5
scale 2
collider capsule 1 0.5
chatid "busstop1clippynpc1"
chat "busstop1clippynpc1"
name "Rudy"
msg 3 INIT cryo "Error: No response"
lvl info
msg 0 cryo EXIT "Lifeform in cryostasis detected."
lvl info
actor 147002e3 165001e3 336e6 "orb_busstop"
relativeto jupiter
id "busstop2"
name "StarTrans Bus Station 'Oscillation Station'"
scale 100
wants maxrotation 0
wants maxvelocity 0
actor 120 864 150 clippy
relativeto "busstop2"
id "busstopclippy2"
name "StarTrans Clippy™"
armodel clippy_ar
angularmomentum 0 0 0
wants maxrotation 0
wants maxvelocity 0
thrust 15 6 3 400 0.5
rotationy -0.5
scale 3
chatid "busstopclippy"
chat "busstopclippy"
actor 40 10 40 "orb_busstop"
name "Light Orb"
relativeto busstopclippy2
light "47FF00" 1000000
actor 30 60 -10 "orb_busstop"
name "Light Orb"
relativeto busstopclippy2
light "47FF00" 1000000
actor -10 -60 20 "orb_busstop"
name "Light Orb"
relativeto busstopclippy2
light "47FF00" 1000000
actor -40 20 30 "orb_busstop"
name "Light Orb"
relativeto busstopclippy2
light "47FF00" 1000000
actor 27643e3 -44e3 -124434e3 "orb_busstop"
relativeto jupiter
id "busstop3"
name "StarTrans Bus Station 'Metis Prime'"
scale 100
wants maxrotation 0
wants maxvelocity 0
actor 120 864 150 clippy
relativeto "busstop3"
id "busstopclippy3"
name "StarTrans Clippy™"
armodel clippy_ar
angularmomentum 0 0 0
wants maxrotation 0
wants maxvelocity 0
thrust 15 6 3 400 0.5
rotationy -0.5
scale 3
chatid "busstopclippy"
chat "busstopclippy"
actor 40 10 40 "orb_busstop"
name "Light Orb"
relativeto busstopclippy3
light "47FF00" 1000000
actor 30 60 -10 "orb_busstop"
name "Light Orb"
relativeto busstopclippy3
light "47FF00" 1000000
actor -10 -60 20 "orb_busstop"
name "Light Orb"
relativeto busstopclippy3
light "47FF00" 1000000
actor -40 20 30 "orb_busstop"
name "Light Orb"
relativeto busstopclippy3
light "47FF00" 1000000
chat error
name ERROR
msg 0 INIT EXIT "Unspecified conversation ID"

View File

@ -8,7 +8,8 @@ impl Plugin for EffectsPlugin {
app.add_systems(Startup, setup);
app.add_systems(Startup, spawn_effects.after(setup).after(camera::setup_camera));
app.add_systems(Update, spawn_effects);
app.add_systems(Update, update_fadeblack);
app.add_systems(Update, update_fadein);
app.add_systems(Update, update_fadeout);
// Blackout disabled for now
//app.add_systems(Update, update_blackout);
app.add_event::<SpawnEffectEvent>();
@ -18,12 +19,14 @@ impl Plugin for EffectsPlugin {
#[derive(Clone)]
pub enum Effects {
FadeIn(Color),
FadeOut(Color),
}
// Blackout disabled for now
//#[derive(Component)] pub struct BlackOutOverlay;
#[derive(Component)] pub struct FadeBlack;
#[derive(Component)] pub struct FadeIn;
#[derive(Component)] pub struct FadeOut;
#[derive(Component)]
pub struct Effect {
pub class: Effects,
@ -76,7 +79,7 @@ pub fn spawn_effects(
duration: effect.duration,
start_time: now,
},
FadeBlack,
FadeIn,
NodeBundle {
style: Style {
width: Val::Vw(100.0),
@ -91,14 +94,36 @@ pub fn spawn_effects(
},
));
},
Effects::FadeOut(color) => {
commands.spawn((
Effect {
class: effect.class.clone(),
duration: effect.duration,
start_time: now,
},
FadeOut,
NodeBundle {
style: Style {
width: Val::Vw(100.0),
height: Val::Vh(100.0),
position_type: PositionType::Absolute,
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
},
background_color: color.with_a(0.0).into(),
..default()
},
));
},
//_ => {},
}
}
}
pub fn update_fadeblack(
pub fn update_fadein(
mut commands: Commands,
mut q_effect: Query<(Entity, &Effect, &mut BackgroundColor), With<FadeBlack>>,
mut q_effect: Query<(Entity, &Effect, &mut BackgroundColor), With<FadeIn>>,
time: Res<Time>,
) {
for (entity, effect, mut bgcolor) in &mut q_effect {
@ -112,6 +137,22 @@ pub fn update_fadeblack(
}
}
pub fn update_fadeout(
mut commands: Commands,
mut q_effect: Query<(Entity, &Effect, &mut BackgroundColor), With<FadeOut>>,
time: Res<Time>,
) {
for (entity, effect, mut bgcolor) in &mut q_effect {
let now = time.elapsed_seconds_f64();
if effect.start_time + effect.duration < now {
commands.entity(entity).despawn();
continue;
}
let alpha = ((now - effect.start_time) / effect.duration).clamp(0.0, 1.0);
bgcolor.0.set_a(alpha as f32);
}
}
// Blackout disabled for now
//pub fn update_blackout(
// mut q_effect: Query<&mut BackgroundColor, With<BlackOutOverlay>>,

View File

@ -24,6 +24,7 @@ impl Plugin for HudPlugin {
app.add_systems(Startup, setup);
app.add_systems(Update, (
update_hud,
update_ar_overlays,
handle_input,
handle_target_event,
));
@ -33,6 +34,9 @@ impl Plugin for HudPlugin {
.after(camera::apply_input_to_player)
.before(TransformSystem::TransformPropagate),
));
app.insert_resource(AugmentedRealityState {
overlays_visible: false,
});
app.insert_resource(Log {
logs: VecDeque::with_capacity(LOG_MAX),
needs_rerendering: true,
@ -52,6 +56,17 @@ impl Plugin for HudPlugin {
#[derive(Component)] struct Selectagon;
#[derive(Component)] pub struct IsTargeted;
#[derive(Resource)]
pub struct AugmentedRealityState {
pub overlays_visible: bool,
}
#[derive(Component)] pub struct AugmentedRealityOverlayBroadcaster;
#[derive(Component)]
pub struct AugmentedRealityOverlay {
pub owner: Entity,
}
#[derive(Resource)]
struct FPSUpdateTimer(Timer);
@ -465,6 +480,14 @@ fn update_hud(
}
}
let dev_speed = if settings.dev_mode {
let x = pos.x;
let y = pos.y;
let z = pos.z;
format!("\n{x:.0}\n{y:.0}\n{z:.0}")
} else {
"".to_string()
};
let gforce = gforce.gforce;
if let Ok((clickable, _, target_v_maybe)) = q_target.get_single() {
let distance = if dist_scalar.is_nan() {
@ -481,12 +504,12 @@ fn update_hud(
};
let speed_readable = nature::readable_distance(speed);
let target_name = clickable.name.clone().unwrap_or("Unnamed".to_string());
text.sections[14].value = format!("\n\nTarget: {target_name}\nDistance: {distance}\nΔv {speed_readable}/s + {gforce:.1}g");
text.sections[14].value = format!("\n\nTarget: {target_name}\nDistance: {distance}\nΔv {speed_readable}/s + {gforce:.1}g{dev_speed}");
}
else {
let speed = cam_v.length();
let speed_readable = nature::readable_distance(speed);
text.sections[14].value = format!("\nv {speed_readable}/s + {gforce:.1}g");
text.sections[14].value = format!("\nv {speed_readable}/s + {gforce:.1}g{dev_speed}");
}
}
}
@ -645,3 +668,39 @@ fn update_target_selectagon(
}
}
}
fn update_ar_overlays (
q_owners: Query<(Entity, &Transform, &Visibility), (With<AugmentedRealityOverlayBroadcaster>, Without<AugmentedRealityOverlay>)>,
mut q_overlays: Query<(&mut Transform, &mut Visibility, &mut AugmentedRealityOverlay)>,
settings: ResMut<settings::Settings>,
mut state: ResMut<AugmentedRealityState>,
) {
let (need_activate, need_clean, need_update);
if settings.hud_active {
need_activate = !state.overlays_visible;
need_clean = false;
}
else {
need_activate = false;
need_clean = state.overlays_visible;
}
need_update = settings.hud_active;
state.overlays_visible = settings.hud_active;
if need_update || need_clean || need_activate {
for (mut trans, mut vis, ar) in &mut q_overlays {
for (owner_id, owner_trans, owner_vis) in &q_owners {
if owner_id == ar.owner {
*trans = *owner_trans;
if need_clean {
*vis = Visibility::Hidden;
}
else {
*vis = *owner_vis;
}
break;
}
}
}
}
}

View File

@ -77,7 +77,7 @@ fn setup(
fn handle_input(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<settings::Settings>,
settings: Res<settings::Settings>,
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
mut windows: Query<&mut Window, With<PrimaryWindow>>,
) {
@ -93,7 +93,4 @@ fn handle_input(
}
}
}
if keyboard_input.just_pressed(settings.key_restart) {
settings.reset();
}
}

View File

@ -19,6 +19,7 @@ pub struct Settings {
pub hud_active: bool,
pub is_zooming: bool,
pub third_person: bool,
pub rotation_stabilizer_active: bool,
pub key_selectobject: MouseButton,
pub key_zoom: MouseButton,
pub key_togglehud: KeyCode,
@ -37,6 +38,7 @@ pub struct Settings {
pub key_vehicle: KeyCode,
pub key_camera: KeyCode,
pub key_rotate: KeyCode,
pub key_rotation_stabilizer: KeyCode,
pub key_mouseup: KeyCode,
pub key_mousedown: KeyCode,
pub key_mouseleft: KeyCode,
@ -101,11 +103,12 @@ impl Default for Settings {
hud_active: false,
is_zooming: false,
third_person: false,
rotation_stabilizer_active: true,
key_selectobject: MouseButton::Left,
key_zoom: MouseButton::Right,
key_togglehud: KeyCode::Tab,
key_exit: KeyCode::Escape,
key_restart: KeyCode::F12,
key_restart: KeyCode::F7,
key_fullscreen: KeyCode::F11,
key_forward: KeyCode::KeyW,
key_back: KeyCode::KeyS,
@ -119,6 +122,7 @@ impl Default for Settings {
key_vehicle: KeyCode::KeyQ,
key_camera: KeyCode::KeyF,
key_rotate: KeyCode::KeyR,
key_rotation_stabilizer: KeyCode::KeyY,
key_mouseup: KeyCode::KeyI,
key_mousedown: KeyCode::KeyK,
key_mouseleft: KeyCode::KeyJ,
@ -139,23 +143,32 @@ impl Default for Settings {
key_cheat_stop: KeyCode::KeyC,
key_cheat_speed: KeyCode::KeyV,
key_cheat_speed_backward: KeyCode::KeyB,
key_cheat_pizza: KeyCode::F8,
key_cheat_farview1: KeyCode::F9,
key_cheat_farview2: KeyCode::F10,
key_cheat_pizza: KeyCode::F9,
key_cheat_farview1: KeyCode::F10,
key_cheat_farview2: KeyCode::F12,
key_cheat_adrenaline_zero: KeyCode::F5,
key_cheat_adrenaline_mid: KeyCode::F6,
key_cheat_adrenaline_max: KeyCode::F7,
key_cheat_adrenaline_max: KeyCode::F8,
key_cheat_die: KeyCode::KeyZ,
}
}
}
impl Settings {
#[allow(dead_code)]
pub fn reset(&mut self) {
println!("Resetting settings!");
*self = Self::default();
}
pub fn reset_player_settings(&mut self) {
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;
}
pub fn get_reply_keys(&self) -> [KeyCode; 10] {
return [
self.key_reply1,

View File

@ -24,16 +24,21 @@ const ASSET_ASTEROID2: &str = "models/asteroid2.glb#Scene0";
pub fn asset_name_to_path(name: &str) -> &'static str {
match name {
"suit" => "models/suit.glb#Scene0",
"suit_ar_chefhat" => "models/suit_ar_chefhat.glb#Scene0",
"asteroid1" => ASSET_ASTEROID1,
"asteroid2" => ASSET_ASTEROID2,
"moonlet" => "models/moonlet.glb#Scene0",
"monolith" => "models/monolith_neon.glb#Scene0",
"lightorb" => "models/lightorb.glb#Scene0",
"orb_busstop" => "models/orb_busstop.glb#Scene0",
"orb_busstop_dim" => "models/orb_busstop_dim.glb#Scene0",
"MeteorAceGT" => "models/MeteorAceGT.glb#Scene0",
"satellite" => "models/satellite.glb#Scene0",
"pizzeria" => "models/pizzeria2.glb#Scene0",
"pizzasign" => "models/pizzasign.glb#Scene0",
"selectagon" => "models/selectagon.glb#Scene0",
"clippy" => "models/clippy.glb#Scene0",
"clippy_ar" => "models/clippy_ar.glb#Scene0",
_ => "models/error.glb#Scene0",
}
}
@ -441,7 +446,8 @@ fn handle_cheats(
lifeform.adrenaline = 1.0;
}
if key_input.just_pressed(settings.key_cheat_die) {
ew_playerdies.send(actor::PlayerDiesEvent);
settings.god_mode = false;
ew_playerdies.send(actor::PlayerDiesEvent(actor::DamageType::Trauma));
}
}