rip out existing chat implementation

(this was SO satisfying)
This commit is contained in:
yuni 2024-04-12 20:39:10 +02:00
parent 5ca31fda65
commit 56385b257d
8 changed files with 473 additions and 629 deletions

View file

@ -2,7 +2,7 @@ use bevy::prelude::*;
use bevy_xpbd_3d::prelude::*; use bevy_xpbd_3d::prelude::*;
use bevy::scene::SceneInstance; use bevy::scene::SceneInstance;
use bevy::math::DVec3; use bevy::math::DVec3;
use crate::{actor, audio, camera, chat2, commands, effects, hud, nature, settings, world}; use crate::{actor, audio, camera, chat, commands, effects, hud, nature, settings, world};
pub const ENGINE_SPEED_FACTOR: f32 = 30.0; pub const ENGINE_SPEED_FACTOR: f32 = 30.0;
const MAX_TRANSMISSION_DISTANCE: f32 = 60.0; const MAX_TRANSMISSION_DISTANCE: f32 = 60.0;
@ -235,11 +235,11 @@ pub fn handle_input(
mut commands: Commands, mut commands: Commands,
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<settings::Settings>, mut settings: ResMut<settings::Settings>,
q_talker: Query<(&chat2::Talker, &Transform), (Without<actor::Player>, Without<Camera>)>, q_talker: Query<(&chat::Talker, &Transform), (Without<actor::Player>, Without<Camera>)>,
player: Query<Entity, With<actor::Player>>, player: Query<Entity, With<actor::Player>>,
q_camera: Query<&Transform, With<Camera>>, q_camera: Query<&Transform, With<Camera>>,
q_vehicles: Query<(Entity, &Transform), (With<actor::Vehicle>, Without<actor::Player>, Without<Camera>)>, q_vehicles: Query<(Entity, &Transform), (With<actor::Vehicle>, Without<actor::Player>, Without<Camera>)>,
mut ew_conv: EventWriter<chat2::StartConversationEvent>, mut ew_conv: EventWriter<chat::StartConversationEvent>,
mut ew_vehicle: EventWriter<VehicleEnterExitEvent>, mut ew_vehicle: EventWriter<VehicleEnterExitEvent>,
mut ew_playerdies: EventWriter<PlayerDiesEvent>, mut ew_playerdies: EventWriter<PlayerDiesEvent>,
q_player_drives: Query<Entity, With<PlayerDrivesThis>>, q_player_drives: Query<Entity, With<PlayerDrivesThis>>,
@ -252,14 +252,14 @@ pub fn handle_input(
if keyboard_input.just_pressed(settings.key_interact) { if keyboard_input.just_pressed(settings.key_interact) {
// Talking to people // Talking to people
let objects: Vec<(chat2::Talker, &Transform)> = q_talker let objects: Vec<(chat::Talker, &Transform)> = q_talker
.iter() .iter()
.map(|(talker, transform)| (talker.clone(), transform)) .map(|(talker, transform)| (talker.clone(), transform))
.collect(); .collect();
// TODO: replace Transform.translation with Position // TODO: replace Transform.translation with Position
if let (Some(talker), dist) = camera::find_closest_target::<chat2::Talker>(objects, camtrans) { if let (Some(talker), dist) = camera::find_closest_target::<chat::Talker>(objects, camtrans) {
if dist <= MAX_TRANSMISSION_DISTANCE { if dist <= MAX_TRANSMISSION_DISTANCE {
ew_conv.send(chat2::StartConversationEvent{talker: talker.clone()}); ew_conv.send(chat::StartConversationEvent{talker: talker.clone()});
} }
} }
// Entering Vehicles // Entering Vehicles

View file

@ -1,79 +1,12 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3;
use crate::{actor, audio, hud, settings, world, effects};
pub struct ChatPlugin; pub struct ChatPlugin;
impl Plugin for ChatPlugin { impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.register_type::<ChatBranch>();
app.add_systems(Update, (
handle_new_conversations,
handle_reply_keys,
handle_send_messages,
handle_conversations,
handle_chat_scripts,
));
app.add_systems(PostUpdate, despawn_old_choices);
app.add_event::<StartConversationEvent>(); app.add_event::<StartConversationEvent>();
app.add_event::<SendMessageEvent>();
app.add_event::<ChatScriptEvent>();
} }
} }
#[derive(Event)]
pub struct StartConversationEvent {
pub talker: Talker,
}
#[derive(Event)]
pub struct SendMessageEvent {
pub conv_id: String,
pub conv_label: String,
pub text: String,
}
#[derive(Event)]
pub struct ChatScriptEvent {
name: String,
param: String,
param2: String,
}
#[derive(Debug)]
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
pub struct ChatBranch {
pub id: String,
pub name: String,
pub label: String,
pub delay: f64,
pub sound: String,
pub level: String,
pub reply: String,
pub goto: String,
pub choice: String,
pub script: String,
pub script_parameter: String,
pub script_parameter2: String,
}
#[derive(Component)]
pub struct Chat {
pub id: String,
pub label: String,
pub timer: f64,
}
#[derive(Component)]
pub struct ChoiceAvailable {
pub conv_id: String,
pub conv_label: String,
pub recipient: String,
pub text: String,
}
#[derive(Component)] #[derive(Component)]
#[derive(Clone)] #[derive(Clone)]
pub struct Talker { pub struct Talker {
@ -82,325 +15,10 @@ pub struct Talker {
} }
impl Default for Talker { fn default() -> Self { Self { impl Default for Talker { fn default() -> Self { Self {
pronoun: "they/them".to_string(), pronoun: "they/them".to_string(),
conv_id: "error".to_string(), conv_id: "undefined".to_string(),
}}} }}}
pub fn handle_new_conversations( #[derive(Event)]
mut commands: Commands, pub struct StartConversationEvent {
mut er_conv: EventReader<StartConversationEvent>, pub talker: Talker,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
q_conv: Query<&Chat>,
time: Res<Time>,
) {
let label = "INIT";
for event in er_conv.read() {
// check for existing chats with this id
let id = &event.talker.conv_id;
let chats: Vec<&Chat> = q_conv.iter().filter(|c| c.id == *id).collect();
if chats.len() > 0 {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Ping));
continue;
}
// no existing chats yet, let's create a new one
commands.spawn((
Chat {
id: id.to_string(),
label: label.to_string(),
timer: time.elapsed_seconds_f64(),
},
world::DespawnOnPlayerDeath,
));
break;
}
}
fn handle_reply_keys(
keyboard_input: Res<ButtonInput<KeyCode>>,
settings: ResMut<settings::Settings>,
q_choices: Query<&ChoiceAvailable>,
mut evwriter_sendmsg: EventWriter<SendMessageEvent>,
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
) {
let mut selected_choice = 1;
'outer: for key in settings.get_reply_keys() {
if keyboard_input.just_pressed(key) {
let mut count = 1;
for choice in &q_choices {
if count == selected_choice {
evwriter_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
evwriter_sendmsg.send(SendMessageEvent {
conv_id: choice.conv_id.clone(),
conv_label: choice.conv_label.clone(),
text: choice.text.clone(),
});
break 'outer;
}
count += 1;
}
}
selected_choice += 1;
}
}
pub fn handle_send_messages(
mut commands: Commands,
mut er_sendmsg: EventReader<SendMessageEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatscript: EventWriter<ChatScriptEvent>,
mut q_conv: Query<(Entity, &mut Chat)>,
time: Res<Time>,
chat_branches: Query<&ChatBranch>,
mut log: ResMut<hud::Log>,
) {
let now = time.elapsed_seconds_f64();
for event in er_sendmsg.read() {
for (entity, mut chat) in &mut q_conv {
if chat.id != event.conv_id {
continue;
}
if event.conv_label == "EXIT" {
info!("Despawning chat.");
commands.entity(entity).despawn();
continue;
}
let branches: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == event.conv_id
&& branch.label == event.conv_label
&& branch.choice == event.text)
.collect();
if branches.len() != 1 {
error!("Expected 1 branch with ID '{}', label '{}', choice '{}', but got {}! Aborting conversation.", event.conv_id, event.conv_label, event.text, branches.len());
continue;
}
let branch = branches[0];
// TODO despawn the choices
if !branch.reply.is_empty() {
match branch.level.as_str() {
"chat" => log.chat(branch.reply.clone(), branch.name.clone()),
"info" => log.info(branch.reply.clone()),
"warn" => log.warning(branch.reply.clone()),
_ => (),
}
}
chat.label = branch.goto.clone();
chat.timer = now + branch.delay;
if branch.sound != "" {
let sfx = audio::str2sfx(branch.sound.as_str());
ew_sfx.send(audio::PlaySfxEvent(sfx));
}
let choices: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id && branch.label == chat.label)
.collect();
for choice in choices {
if choice.choice.as_str() != hud::CHOICE_NONE {
commands.spawn((
ChoiceAvailable {
conv_id: choice.id.clone(),
conv_label: choice.label.clone(),
recipient: choice.name.clone(),
text: choice.choice.clone(),
},
world::DespawnOnPlayerDeath,
));
}
}
if !branch.script.is_empty() {
ew_chatscript.send(ChatScriptEvent {
name: branch.script.clone(),
param: branch.script_parameter.clone(),
param2: branch.script_parameter2.clone(),
});
}
}
break; // let's only handle one of these per frame
}
}
pub fn handle_conversations(
mut commands: Commands,
mut log: ResMut<hud::Log>,
mut q_conv: Query<(Entity, &mut Chat)>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatscript: EventWriter<ChatScriptEvent>,
time: Res<Time>,
chat_branches: Query<&ChatBranch>, // TODO: use Table for faster iteration?
) {
let now = time.elapsed_seconds_f64();
for (entity, mut chat) in &mut q_conv {
if chat.label == "EXIT" {
info!("Despawning chat.");
commands.entity(entity).despawn();
continue;
}
if now < chat.timer {
continue;
}
let branches: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id
&& branch.label == chat.label
&& branch.choice == "")
.collect();
if branches.len() != 1 {
error!("Expected 1 branch with ID '{}' and label '{}', but got {}! Aborting conversation.", chat.id, chat.label, branches.len());
continue;
}
let branch = branches[0];
if !branch.reply.is_empty() {
match branch.level.as_str() {
"chat" => log.chat(branch.reply.clone(), branch.name.clone()),
"info" => log.info(branch.reply.clone()),
"warn" => log.warning(branch.reply.clone()),
_ => (),
}
}
if chat.label == "EXIT" {
// TODO: isn't this dead code?
continue;
}
chat.label = branch.goto.clone();
if branch.sound != "" {
let sfx = audio::str2sfx(branch.sound.as_str());
ew_sfx.send(audio::PlaySfxEvent(sfx));
}
chat.timer = now + branch.delay;
let choices: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id && branch.label == chat.label)
.collect();
for choice in choices {
if choice.choice.as_str() != hud::CHOICE_NONE {
commands.spawn((
ChoiceAvailable {
conv_id: choice.id.clone(),
conv_label: choice.label.clone(),
recipient: choice.name.clone(),
text: choice.choice.clone(),
},
world::DespawnOnPlayerDeath,
));
}
}
if !branch.script.is_empty() {
ew_chatscript.send(ChatScriptEvent {
name: branch.script.clone(),
param: branch.script_parameter.clone(),
param2: branch.script_parameter2.clone(),
});
}
}
}
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, &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 suit, _) in q_player.iter_mut() {
if script.param2.is_empty() {
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
}
else {
let mut found_other = false;
info!("param2={}", script.param2);
for (other_actor, mut other_suit) in q_actor.iter_mut() {
if !other_actor.id.is_empty() {
info!("ID={}", other_actor.id);
}
if other_actor.id == script.param2 {
found_other = true;
amount = amount
.clamp(0.0, other_suit.oxygen)
.clamp(0.0, suit.oxygen_max - suit.oxygen);
other_suit.oxygen = other_suit.oxygen - amount;
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
break;
}
}
if !found_other {
error!("Script error: could not find actor with ID `{}`", script.param2);
}
}
}
} 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}");
}
}
}
}
fn despawn_old_choices(
mut commands: Commands,
q_conv: Query<&Chat>,
q_choices: Query<(Entity, &ChoiceAvailable)>,
) {
let chats: Vec<&Chat> = q_conv.iter().collect();
'outer: for (entity, choice) in &q_choices {
// Let's see if this choice still has a chat in the appropriate state
for chat in &chats {
if choice.conv_id == chat.id && choice.conv_label == chat.label {
continue 'outer;
}
}
// Despawn the choice, since no matching chat was found
commands.entity(entity).despawn();
}
} }

406
src/chat_old.rs Normal file
View file

@ -0,0 +1,406 @@
use bevy::prelude::*;
use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3;
use crate::{actor, audio, hud, settings, world, effects};
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.register_type::<ChatBranch>();
app.add_systems(Update, (
handle_new_conversations,
handle_reply_keys,
handle_send_messages,
handle_conversations,
handle_chat_scripts,
));
app.add_systems(PostUpdate, despawn_old_choices);
app.add_event::<StartConversationEvent>();
app.add_event::<SendMessageEvent>();
app.add_event::<ChatScriptEvent>();
}
}
#[derive(Event)]
pub struct StartConversationEvent {
pub talker: Talker,
}
#[derive(Event)]
pub struct SendMessageEvent {
pub conv_id: String,
pub conv_label: String,
pub text: String,
}
#[derive(Event)]
pub struct ChatScriptEvent {
name: String,
param: String,
param2: String,
}
#[derive(Debug)]
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
pub struct ChatBranch {
pub id: String,
pub name: String,
pub label: String,
pub delay: f64,
pub sound: String,
pub level: String,
pub reply: String,
pub goto: String,
pub choice: String,
pub script: String,
pub script_parameter: String,
pub script_parameter2: String,
}
#[derive(Component)]
pub struct Chat {
pub id: String,
pub label: String,
pub timer: f64,
}
#[derive(Component)]
pub struct ChoiceAvailable {
pub conv_id: String,
pub conv_label: String,
pub recipient: String,
pub text: String,
}
#[derive(Component)]
#[derive(Clone)]
pub struct Talker {
pub pronoun: String,
pub conv_id: String,
}
impl Default for Talker { fn default() -> Self { Self {
pronoun: "they/them".to_string(),
conv_id: "error".to_string(),
}}}
pub fn handle_new_conversations(
mut commands: Commands,
mut er_conv: EventReader<StartConversationEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
q_conv: Query<&Chat>,
time: Res<Time>,
) {
let label = "INIT";
for event in er_conv.read() {
// check for existing chats with this id
let id = &event.talker.conv_id;
let chats: Vec<&Chat> = q_conv.iter().filter(|c| c.id == *id).collect();
if chats.len() > 0 {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Ping));
continue;
}
// no existing chats yet, let's create a new one
commands.spawn((
Chat {
id: id.to_string(),
label: label.to_string(),
timer: time.elapsed_seconds_f64(),
},
world::DespawnOnPlayerDeath,
));
break;
}
}
fn handle_reply_keys(
keyboard_input: Res<ButtonInput<KeyCode>>,
settings: ResMut<settings::Settings>,
q_choices: Query<&ChoiceAvailable>,
mut evwriter_sendmsg: EventWriter<SendMessageEvent>,
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
) {
let mut selected_choice = 1;
'outer: for key in settings.get_reply_keys() {
if keyboard_input.just_pressed(key) {
let mut count = 1;
for choice in &q_choices {
if count == selected_choice {
evwriter_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
evwriter_sendmsg.send(SendMessageEvent {
conv_id: choice.conv_id.clone(),
conv_label: choice.conv_label.clone(),
text: choice.text.clone(),
});
break 'outer;
}
count += 1;
}
}
selected_choice += 1;
}
}
pub fn handle_send_messages(
mut commands: Commands,
mut er_sendmsg: EventReader<SendMessageEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatscript: EventWriter<ChatScriptEvent>,
mut q_conv: Query<(Entity, &mut Chat)>,
time: Res<Time>,
chat_branches: Query<&ChatBranch>,
mut log: ResMut<hud::Log>,
) {
let now = time.elapsed_seconds_f64();
for event in er_sendmsg.read() {
for (entity, mut chat) in &mut q_conv {
if chat.id != event.conv_id {
continue;
}
if event.conv_label == "EXIT" {
info!("Despawning chat.");
commands.entity(entity).despawn();
continue;
}
let branches: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == event.conv_id
&& branch.label == event.conv_label
&& branch.choice == event.text)
.collect();
if branches.len() != 1 {
error!("Expected 1 branch with ID '{}', label '{}', choice '{}', but got {}! Aborting conversation.", event.conv_id, event.conv_label, event.text, branches.len());
continue;
}
let branch = branches[0];
// TODO despawn the choices
if !branch.reply.is_empty() {
match branch.level.as_str() {
"chat" => log.chat(branch.reply.clone(), branch.name.clone()),
"info" => log.info(branch.reply.clone()),
"warn" => log.warning(branch.reply.clone()),
_ => (),
}
}
chat.label = branch.goto.clone();
chat.timer = now + branch.delay;
if branch.sound != "" {
let sfx = audio::str2sfx(branch.sound.as_str());
ew_sfx.send(audio::PlaySfxEvent(sfx));
}
let choices: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id && branch.label == chat.label)
.collect();
for choice in choices {
if choice.choice.as_str() != hud::CHOICE_NONE {
commands.spawn((
ChoiceAvailable {
conv_id: choice.id.clone(),
conv_label: choice.label.clone(),
recipient: choice.name.clone(),
text: choice.choice.clone(),
},
world::DespawnOnPlayerDeath,
));
}
}
if !branch.script.is_empty() {
ew_chatscript.send(ChatScriptEvent {
name: branch.script.clone(),
param: branch.script_parameter.clone(),
param2: branch.script_parameter2.clone(),
});
}
}
break; // let's only handle one of these per frame
}
}
pub fn handle_conversations(
mut commands: Commands,
mut log: ResMut<hud::Log>,
mut q_conv: Query<(Entity, &mut Chat)>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatscript: EventWriter<ChatScriptEvent>,
time: Res<Time>,
chat_branches: Query<&ChatBranch>, // TODO: use Table for faster iteration?
) {
let now = time.elapsed_seconds_f64();
for (entity, mut chat) in &mut q_conv {
if chat.label == "EXIT" {
info!("Despawning chat.");
commands.entity(entity).despawn();
continue;
}
if now < chat.timer {
continue;
}
let branches: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id
&& branch.label == chat.label
&& branch.choice == "")
.collect();
if branches.len() != 1 {
error!("Expected 1 branch with ID '{}' and label '{}', but got {}! Aborting conversation.", chat.id, chat.label, branches.len());
continue;
}
let branch = branches[0];
if !branch.reply.is_empty() {
match branch.level.as_str() {
"chat" => log.chat(branch.reply.clone(), branch.name.clone()),
"info" => log.info(branch.reply.clone()),
"warn" => log.warning(branch.reply.clone()),
_ => (),
}
}
if chat.label == "EXIT" {
// TODO: isn't this dead code?
continue;
}
chat.label = branch.goto.clone();
if branch.sound != "" {
let sfx = audio::str2sfx(branch.sound.as_str());
ew_sfx.send(audio::PlaySfxEvent(sfx));
}
chat.timer = now + branch.delay;
let choices: Vec<&ChatBranch> = chat_branches.iter()
.filter(|branch| branch.id == chat.id && branch.label == chat.label)
.collect();
for choice in choices {
if choice.choice.as_str() != hud::CHOICE_NONE {
commands.spawn((
ChoiceAvailable {
conv_id: choice.id.clone(),
conv_label: choice.label.clone(),
recipient: choice.name.clone(),
text: choice.choice.clone(),
},
world::DespawnOnPlayerDeath,
));
}
}
if !branch.script.is_empty() {
ew_chatscript.send(ChatScriptEvent {
name: branch.script.clone(),
param: branch.script_parameter.clone(),
param2: branch.script_parameter2.clone(),
});
}
}
}
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, &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 suit, _) in q_player.iter_mut() {
if script.param2.is_empty() {
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
}
else {
let mut found_other = false;
info!("param2={}", script.param2);
for (other_actor, mut other_suit) in q_actor.iter_mut() {
if !other_actor.id.is_empty() {
info!("ID={}", other_actor.id);
}
if other_actor.id == script.param2 {
found_other = true;
amount = amount
.clamp(0.0, other_suit.oxygen)
.clamp(0.0, suit.oxygen_max - suit.oxygen);
other_suit.oxygen = other_suit.oxygen - amount;
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
break;
}
}
if !found_other {
error!("Script error: could not find actor with ID `{}`", script.param2);
}
}
}
} 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}");
}
}
}
}
fn despawn_old_choices(
mut commands: Commands,
q_conv: Query<&Chat>,
q_choices: Query<(Entity, &ChoiceAvailable)>,
) {
let chats: Vec<&Chat> = q_conv.iter().collect();
'outer: for (entity, choice) in &q_choices {
// Let's see if this choice still has a chat in the appropriate state
for chat in &chats {
if choice.conv_id == chat.id && choice.conv_label == chat.label {
continue 'outer;
}
}
// Despawn the choice, since no matching chat was found
commands.entity(entity).despawn();
}
}

View file

@ -16,6 +16,14 @@
--- ---
- chat: Rudy
- system: "Error: No response"
- system: Lifeform in cryostasis detected
---
- chat: ClippyTransSerenity - chat: ClippyTransSerenity
- Welcome at StarTrans Cargo Services! You have reached Serenity Station. - Welcome at StarTrans Cargo Services! You have reached Serenity Station.
- "Ready for a trip? Available bus stops: Oscillation Station, Metis Prime Station" - "Ready for a trip? Available bus stops: Oscillation Station, Metis Prime Station"

View file

@ -3,7 +3,7 @@ extern crate regex;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_xpbd_3d::prelude::*; use bevy_xpbd_3d::prelude::*;
use bevy::math::DVec3; use bevy::math::DVec3;
use crate::{actor, chat2, hud, nature, world}; use crate::{actor, chat, hud, nature, world};
use regex::Regex; use regex::Regex;
use std::f32::consts::PI; use std::f32::consts::PI;
use std::f64::consts::PI as PI64; use std::f64::consts::PI as PI64;
@ -22,7 +22,6 @@ impl Plugin for CommandsPlugin {
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
enum DefClass { enum DefClass {
Actor, Actor,
Chat,
None, None,
} }
@ -69,18 +68,18 @@ struct ParserState {
light_color: Option<Color>, light_color: Option<Color>,
ar_model: Option<String>, ar_model: Option<String>,
// Chat fields // // Chat fields
delay: f64, // delay: f64,
text: String, // text: String,
level: String, // level: String,
label: String, // label: String,
goto: String, // goto: String,
is_choice: bool, // is_choice: bool,
stores_item: bool, // stores_item: bool,
script: String, // script: String,
script_parameter: String, // script_parameter: String,
script_parameter2: String, // script_parameter2: String,
sound: Option<String>, // sound: Option<String>,
} }
impl Default for ParserState { impl Default for ParserState {
fn default() -> Self { fn default() -> Self {
@ -125,34 +124,34 @@ impl Default for ParserState {
light_color: None, light_color: None,
ar_model: None, ar_model: None,
delay: 0.0, // delay: 0.0,
text: "".to_string(), // text: "".to_string(),
level: "chat".to_string(), // level: "chat".to_string(),
label: "".to_string(), // label: "".to_string(),
goto: "".to_string(), // goto: "".to_string(),
is_choice: false, // is_choice: false,
stores_item: false, // stores_item: false,
script: "".to_string(), // script: "".to_string(),
script_parameter: "".to_string(), // script_parameter: "".to_string(),
script_parameter2: "".to_string(), // script_parameter2: "".to_string(),
sound: Some("chat".to_string()), // sound: Some("chat".to_string()),
} }
} }
} }
impl ParserState { impl ParserState {
fn reset_message(&mut self) { // fn reset_message(&mut self) {
let default = ParserState::default(); // let default = ParserState::default();
self.label = default.label; // self.label = default.label;
self.delay = default.delay; // self.delay = default.delay;
self.goto = default.goto; // self.goto = default.goto;
self.level = default.level; // self.level = default.level;
self.text = default.text; // self.text = default.text;
self.is_choice = default.is_choice; // self.is_choice = default.is_choice;
self.script = default.script; // self.script = default.script;
self.script_parameter = default.script_parameter; // self.script_parameter = default.script_parameter;
self.script_parameter2 = default.script_parameter2; // self.script_parameter2 = default.script_parameter2;
self.sound = default.sound; // self.sound = default.sound;
} // }
// fn as_chatbranch(&self) -> chat::ChatBranch { // fn as_chatbranch(&self) -> chat::ChatBranch {
// return chat::ChatBranch { // return chat::ChatBranch {
// id: self.chat.clone(), // id: self.chat.clone(),
@ -217,6 +216,11 @@ pub fn load_defs(
} }
match parts.as_slice() { match parts.as_slice() {
["name", name] => {
debug!("Registering name: {}", name);
state.name = Some(name.to_string());
}
// Parsing actors // Parsing actors
["actor", x, y, z, model] => { ["actor", x, y, z, model] => {
ew_spawn.send(SpawnEvent(state)); ew_spawn.send(SpawnEvent(state));
@ -475,10 +479,6 @@ pub fn load_defs(
// state.class = DefClass::Chat; // state.class = DefClass::Chat;
// state.chat = chat_name.to_string(); // state.chat = chat_name.to_string();
// } // }
// ["name", name] => {
// debug!("Registering name: {}", name);
// state.name = Some(name.to_string());
// }
// ["msg", sleep, text] => { // ["msg", sleep, text] => {
// debug!("Registering message (sleep={}): {}", sleep, text); // debug!("Registering message (sleep={}): {}", sleep, text);
// ew_spawn.send(SpawnEvent(state.clone())); // ew_spawn.send(SpawnEvent(state.clone()));
@ -568,9 +568,9 @@ pub fn load_defs(
// ["sound", "none"] => { // ["sound", "none"] => {
// state.sound = None; // state.sound = None;
// } // }
// _ => { _ => {
// error!("No match for [{}]", parts.join(",")); error!("No match for [{}]", parts.join(","));
// } }
} }
} }
ew_spawn.send(SpawnEvent(state)); ew_spawn.send(SpawnEvent(state));
@ -691,7 +691,7 @@ fn spawn_entities(
}); });
} }
if !state.chat.is_empty() { if !state.chat.is_empty() {
actor.insert(chat2::Talker { actor.insert(chat::Talker {
conv_id: state.chat.clone(), conv_id: state.chat.clone(),
..default() ..default()
}); });

View file

@ -180,9 +180,6 @@ actor -3300 10 0 pizzeria
rotationy -0.5 rotationy -0.5
scale 3 scale 3
chatid pizzaclippy chatid pizzaclippy
chat pizzaclippy
name "Clippy™ Convenience Companion"
msg 4 INIT EXIT "At your service!"
actor -35 0 0 suit actor -35 0 0 suit
relativeto pizzeria relativeto pizzeria
@ -198,50 +195,6 @@ actor -3300 10 0 pizzeria
rotationy 1 rotationy 1
angularmomentum 0 0 0 angularmomentum 0 0 0
pronoun he pronoun he
chat pizzeria
name "Space Pizza™"
msg 4 INIT cool "Welcome to Space Pizza™, best pizza around the rings!"
msg 4 cool order "Great to see a customer, we don't get many lately"
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 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?"
msg 6 anyway pass "An emulsion of deliciousness!"
msg 30 tube pass "Deliciousness for your spacesuit feeding tube!"
choice 3 pass yourloss "I think I'll pass..."
msg 3 yourloss end "Your loss, mate"
msg 3 pass prank "Hey? Are you still there?"
choice 3 special carved "Wh... what's a pizzeria doing here?"
msg 6 carved work "Hah, beautiful, right? I carved it out this asteroid myself!"
msg 7 work nowwannaorder "You know how much work it was to neutralize the rotation of the asteroid, so my valued customers don't bang against the walls while drinking my pizza?"
msg 20 nowwannaorder special "Now would you like today's special or not?"
choice 3 special ohno "My head hurts, my suit leaks, I think I'm dying..."
msg 20 ohno pressok "Seriously? Let me have a look. Just press the 'Grant Access' button please."
choice 3 pressok virus "[GRANT ACCESS TO SPACESUIT WIFI]"
msg 3 virus bitcoin "MALWARE DETECTED"
lvl warn
msg 6 bitcoin wtf "BITCOIN MINER DETECTED"
lvl warn
choice 5 wtf justchecking "Hey, what are you doing with me?"
msg 6 justchecking suitfucked "Just checking your systems, hang on tight"
msg 5 suitfucked wtfanyway "Yeah, suit's fucked, I'd look out for a repair shop"
msg 5 wtf wtfanyway "Yeah, suit's fucked, I'd look out for a repair shop"
msg 5 wtfanyway special "Anyway, wanna order today's special?"
choice 3 pressok deny "[DENY ACCESS TO SPACESUIT WIFI]"
msg 5 deny pressok2 "Oh come on, do you want my help or not?"
choice 3 pressok2 virus "[GRANT ACCESS TO SPACESUIT WIFI]"
choice 3 pressok2 deny2 "[DENY ACCESS TO SPACESUIT WIFI]"
choice 3 pressok2 deny2 "Fuck off!"
msg 2 deny2 end "Great, the first customer in ages, and they're brain damaged..."
msg 3 pressok2 prank "Hey? Are you still there?"
msg 3 pressok prank "Hey? Are you still there?"
msg 3 special prank "Hey? Are you still there?"
msg 4 prank end "This a prank?"
msg 0 end EXIT "Disconnected."
lvl info
actor 60 -15 -40 suit actor 60 -15 -40 suit
relativeto player relativeto player
@ -257,90 +210,6 @@ actor 60 -15 -40 suit
wants maxrotation 0.5 wants maxrotation 0.5
wants maxvelocity 0 wants maxvelocity 0
pronoun it pronoun it
chat hi_icarus
name Icarus
msg 4 INIT hi1 "Oh hey, you're awake!"
msg 6 hi1 hi2 "I found you drifting out cold, and thought, I better watch over you."
msg 40 hi2 hireply "Took us here behind that moonlet, to shield you from the micros."
choice 1 hireply thx "Thank you!"
msg 6 thx feeling "No worries. Folks are stretched thin around this corner, we gotta watch out for each other."
msg 40 feeling howru "How are you feeling?"
choice 1 hireply didntask "Eh... I didn't ask for this."
msg 6 didntask problem "Sure, 'cause you were unconscious. I just did what felt right. Is there a problem?"
choice 1 problem thx "Nevermind. Thank you."
choice 1 problem end "Leave me alone!"
choice 1 hireply micros "Micros? What's that?"
msg 40 micros microsexplained "Micrometeorites. Those tiny 混蛋 that fly right through you, leaving holes in your suit. And your body."
choice 1 microsexplained thx "Ouch! Thank you so much."
choice 1 microsexplained didntask "Whatever... I didn't ask for this."
msg 40 microsexplained howru "How are you feeling?"
msg 40 hireply howru "How are you feeling?"
choice 1 howru good1 "I feel quite cozy, this space suit feels like a second skin."
msg 4 good1 good2 "Hah, it does, doesn't it?"
msg 4 good2 good3 "But take care, your suit seems to be leaking. I'd patch it up if I were you."
msg 4 good3 good4 "I'm all out of SuitPatch™ SuperGlue™ right now, otherwise i'd share."
msg 40 good4 canihelp "Can I help you with anything else, maybe?"
choice 1 howru headache1 "I got this apocalyptic headache..."
msg 4 headache1 headache2 "Heh, probably related to why you were passed out."
msg 4 headache2 headache3 "Go easy on yourself, I'm sure things will turn for the better."
msg 40 headache3 canihelp "Meanwhile, can I help you with anything?"
choice 1 howru disoriented "I... don't know, I'm pretty disoriented."
msg 40 disoriented canihelp "Oh no. Do you need a lowdown on reality?"
choice 1 canihelp where1 "Where are we?"
msg 4 where1 where2 "This is space, my friend."
msg 4 where2 where3 "That massive crescent over there, that's Juptiter."
msg 4 where3 where4 "We're about 150,000km away from it, on the very outside of it's rings."
msg 4 where4 where5 "This area is called the Thebe gossamer ring."
msg 4 where5 where6 "The moon Thebe is actually pretty close right now, flinging all those micros at us."
choice 4 where6 where6micros "Micros? What's that?"
msg 4 where6micros canihelp "Micrometeorites. Those tiny 混蛋 that fly right through you, leaving holes in your suit. And your body."
msg 40 where6 canihelp "Anything else?"
choice 1 canihelp year1 "What year is this?"
msg 4 year1 year2 "Oh, is your Augmented Reality deactivated?"
msg 40 year2 canihelp "Push the TAB button, your space suit's AR will show you the date and time."
choice 1 canihelp why1 "Why am I here?"
msg 4 why1 why2 "That's a very philosophical question."
msg 4 why2 why3 "I don't know."
msg 40 why3 canihelp "It's probably related to the choices you made so far."
choice 1 canihelp whatnow1 "What should I do?"
msg 4 whatnow1 whatnow2 "Ah, that's the beauty of life."
msg 4 whatnow2 whatnow3 "You can just do whatever you want."
msg 6 whatnow3 whatnow4 "So long as you have the means, and respect people's boundaries."
msg 4 whatnow4 whatnow5 "I'm here mostly for the view and the peace."
msg 4 whatnow5 whatnow6 "Just look at Jupiter, it's mesmerizing, isn't it?"
msg 7 whatnow6 whatnow7 "So far away from everything, nobody expects anything from you."
msg 6 whatnow7 whatnow8 "If you want, you can take my sports racing capsule MeteorAceGT™ for a ride. It's right over there."
msg 8 whatnow8 whatnow9 "It rides like a punch in the face, don't hurt yourself, ok?"
choice 1 whatnow9 whatnow9tookind "You're too kind!"
msg 4 whatnow9tookind whatnow10 "Ah, don't mention it!"
msg 40 whatnow10 canihelp "There's also a half-decent pizza restaurant over there, look for the neon sign."
msg 40 whatnow9 canihelp "There's also a half-decent pizza restaurant over there, look for the neon sign."
choice 1 canihelp money1 "Do you have some money for me?"
msg 40 money1 money2 "Huh? What is money?"
choice 1 money2 money2currency "Currency? Flat round things that you can exchange for goods and services?"
msg 4 money2currency money2currency2 "Uhm... are you talking about pizza?"
msg 4 money2currency2 money2currency3 "I don't have any pizza with me right now."
msg 4 money2currency3 canihelp "But there's a pizza place right over there, look for the neon sign."
msg 40 money2 canihelp "Well, anyway, need anything else?"
choice 1 canihelp chocolate "I think I'm good for now"
msg 4 canihelp chocolate "Well, anyway."
choice 1 howru alone "I just want to be alone right now"
msg 4 alone end_pizza "Oh, sure. Ping me if you need anything. I'll go back to playing my VR game."
msg 4 howru chocolate "Well, I hope you're ok."
msg 40 chocolate wantchocolate "I got some left-over instant hot chocolate, would you like some?"
choice 1 wantchocolate yeschocolate "Oh yes! Please!"
msg 4 yeschocolate chocolatehere "Here you go, my friend!"
msg 4 chocolatehere pizza "Received 1x ChuggaChug™ Instant Hot Chocolate"
lvl info
msg 4 wantchocolate pizza "I guess not. Well. I think I'll go back to playing my VR game. Ping me if you need anything."
msg 10 pizza end_pizza "Oh and make sure to check out the pizza place!"
choice 1 end_pizza end2 "Will do, bye!"
msg 0 end2 EXIT "Disconnected."
lvl info
msg 0 end_pizza EXIT "Disconnected."
lvl info
msg 0 end EXIT "Disconnected."
lvl info
actor -300 0 40 suit actor -300 0 40 suit
relativeto player relativeto player
@ -350,16 +219,6 @@ actor -300 0 40 suit
oxygen 0.08 oxygen 0.08
scale 2 scale 2
collider capsule 1 0.5 collider capsule 1 0.5
chat drifter
name "Drifter"
msg 2 INIT dead "Error: No response"
lvl info
msg 15 dead outcold "No life signs detected"
lvl info
choice 0 outcold EXIT "Damn, it's gotta be moldy in that suit. How long has it been drifting?"
choice 0 outcold EXIT "Harvest some oxygen"
script refilloxygen 1 drifter
msg 0 outcold EXIT ""
actor 100 -18000 2000 "orb_busstop" actor 100 -18000 2000 "orb_busstop"
relativeto player relativeto player
@ -380,41 +239,6 @@ actor 100 -18000 2000 "orb_busstop"
rotationy -0.5 rotationy -0.5
scale 3 scale 3
chatid "busstopclippy" 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" actor 40 10 40 "orb_busstop"
name "Light Orb" name "Light Orb"
relativeto busstopclippy relativeto busstopclippy
@ -440,12 +264,6 @@ actor 100 -18000 2000 "orb_busstop"
scale 2 scale 2
collider capsule 1 0.5 collider capsule 1 0.5
chatid "busstop1clippynpc1" 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" actor 147002e3 165001e3 336e6 "orb_busstop"
relativeto jupiter relativeto jupiter
@ -466,7 +284,6 @@ actor 147002e3 165001e3 336e6 "orb_busstop"
rotationy -0.5 rotationy -0.5
scale 3 scale 3
chatid "busstopclippy" chatid "busstopclippy"
chat "busstopclippy"
actor 40 10 40 "orb_busstop" actor 40 10 40 "orb_busstop"
name "Light Orb" name "Light Orb"
relativeto busstopclippy2 relativeto busstopclippy2
@ -503,7 +320,6 @@ actor 27643e3 -44e3 -124434e3 "orb_busstop"
rotationy -0.5 rotationy -0.5
scale 3 scale 3
chatid "busstopclippy" chatid "busstopclippy"
chat "busstopclippy"
actor 40 10 40 "orb_busstop" actor 40 10 40 "orb_busstop"
name "Light Orb" name "Light Orb"
relativeto busstopclippy3 relativeto busstopclippy3
@ -520,7 +336,3 @@ actor 27643e3 -44e3 -124434e3 "orb_busstop"
name "Light Orb" name "Light Orb"
relativeto busstopclippy3 relativeto busstopclippy3
light "47FF00" 1000000 light "47FF00" 1000000
chat error
name ERROR
msg 0 INIT EXIT "Unspecified conversation ID"

View file

@ -413,7 +413,7 @@ fn update_hud(
mut timer: ResMut<FPSUpdateTimer>, mut timer: ResMut<FPSUpdateTimer>,
mut query: Query<&mut Text, With<GaugesText>>, mut query: Query<&mut Text, With<GaugesText>>,
//q_choices: Query<&chat::ChoiceAvailable>, //q_choices: Query<&chat::ChoiceAvailable>,
mut query_chat: Query<&mut Text, (With<ChatText>, Without<GaugesText>)>, //mut query_chat: Query<&mut Text, (With<ChatText>, Without<GaugesText>)>,
query_all_actors: Query<&actor::Actor>, query_all_actors: Query<&actor::Actor>,
settings: Res<settings::Settings>, settings: Res<settings::Settings>,
q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>, q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>,

View file

@ -1,7 +1,7 @@
mod actor; mod actor;
mod audio; mod audio;
mod camera; mod camera;
mod chat2; mod chat;
mod commands; mod commands;
mod effects; mod effects;
mod hud; mod hud;
@ -55,7 +55,7 @@ impl Plugin for OutFlyPlugin {
actor::ActorPlugin, actor::ActorPlugin,
audio::AudioPlugin, audio::AudioPlugin,
camera::CameraPlugin, camera::CameraPlugin,
chat2::ChatPlugin, chat::ChatPlugin,
commands::CommandsPlugin, commands::CommandsPlugin,
effects::EffectsPlugin, effects::EffectsPlugin,
hud::HudPlugin, hud::HudPlugin,