outfly/src/chat.rs

968 lines
36 KiB
Rust

// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
//
// This module loads the chat definitions from the YAML files
// and manages the flow of conversations.
use crate::prelude::*;
use bevy::color::palettes::css;
use bevy::prelude::*;
use bevy_xpbd_3d::prelude::*;
use serde::Deserialize;
use serde_yaml::Value;
use std::collections::HashMap;
pub const CHATS: &[&str] = &[
include_str!("chats/serenity.yaml"),
include_str!("chats/startrans.yaml"),
include_str!("chats/thebe.yaml"),
];
pub const TOKEN_CHAT: &str = "chat";
pub const TOKEN_MSG: &str = "msg";
pub const TOKEN_SYSTEM: &str = "system";
pub const TOKEN_WARN: &str = "warn";
pub const TOKEN_SLEEP: &str = "sleep";
pub const TOKEN_SET: &str = "set";
pub const TOKEN_IF: &str = "if";
pub const TOKEN_THEN: &str = "then";
pub const TOKEN_GOTO: &str = "goto";
pub const TOKEN_LABEL: &str = "label";
pub const TOKEN_SCRIPT: &str = "script";
pub const TOKEN_SOUND: &str = "sound";
pub const TOKEN_NOWAIT: &str = "nowait";
pub const TOKEN_INCLUDE: &str = "include";
pub const TOKEN_GOTO_EXIT: &str = "EXIT";
pub const TOKEN_IF_INLINE: &str = "if "; // for lines like `- if foo:`
pub const DEFAULT_SOUND: &str = "chat";
pub const MAX_BRANCH_DEPTH: usize = 64;
pub const CHOICE_TIMER: f64 = 40.0 * var::DEFAULT_CHAT_SPEED as f64;
pub const LETTERS_PER_SECOND: f32 = 17.0;
pub const TALKER_SPEED_FACTOR: f32 = var::DEFAULT_CHAT_SPEED / LETTERS_PER_SECOND;
pub const CHAT_SPEED_MIN_LEN: f32 = 40.0;
pub const NON_CHOICE_TOKENS: &[&str] = &[
TOKEN_CHAT,
TOKEN_MSG,
TOKEN_SYSTEM,
TOKEN_WARN,
TOKEN_SLEEP,
TOKEN_SET,
TOKEN_IF,
TOKEN_THEN,
TOKEN_GOTO,
TOKEN_LABEL,
TOKEN_SCRIPT,
TOKEN_SOUND,
TOKEN_NOWAIT,
];
pub const SKIPPABLE_TOKENS: &[&str] = &[TOKEN_CHAT, TOKEN_LABEL, TOKEN_GOTO, TOKEN_NOWAIT];
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, load_chats);
app.add_systems(
Update,
(
handle_reply_keys.before(handle_chat_timer),
handle_chat_timer.before(handle_chat_events),
handle_new_conversations.before(handle_chat_events),
handle_chat_events.before(handle_chat_scripts),
handle_chat_scripts,
update_chat_variables,
),
);
app.add_event::<StartConversationEvent>();
app.add_event::<ChatEvent>();
app.add_event::<ChatScriptEvent>();
app.insert_resource(ChatDB(Vec::new()));
}
}
type ChatPos = Vec<usize>;
#[derive(Component)]
pub struct Chat {
pub internal_id: usize,
pub position: ChatPos,
pub timer: f64,
pub talker: Talker,
}
#[derive(Component)]
pub struct Choice {
pub text: String,
pub key: usize,
pub goto: ChatPos,
}
#[derive(Component, Clone)]
pub struct Talker {
pub chat_name: String,
pub actor_id: String,
pub name: Option<String>,
pub pronoun: Option<String>,
pub talking_speed: f32,
}
#[derive(Event)]
pub struct StartConversationEvent {
pub talker: Talker,
}
#[derive(Event)]
pub struct ChatScriptEvent(String);
#[derive(Event)]
pub enum ChatEvent {
DespawnAllChoices,
DespawnAllChats,
SpawnMessage(String, hud::LogLevel, String),
SpawnChoice(String, usize, ChatPos, bool, Option<String>),
RunScript(String),
SleepSeconds(f64),
SetVariable(String),
GotoIf(String, ChatPos),
}
pub struct Extracted {
choice_text: Option<String>,
nowait: bool,
condition: Option<String>,
}
impl Default for Extracted {
fn default() -> Self {
Self {
choice_text: None,
nowait: false,
condition: None,
}
}
}
// This is the only place where any YAML interaction should be happening.
#[derive(Resource)]
pub struct ChatDB(Vec<Value>);
impl ChatDB {
pub fn load_from_str(&mut self, yaml_string: &str) -> Result<(), ()> {
let mut count = 0;
for document in serde_yaml::Deserializer::from_str(yaml_string) {
match Value::deserialize(document) {
Ok(yaml_data) => {
if let Value::Sequence(yaml_sequence) = yaml_data {
self.0.push(Value::Sequence(yaml_sequence));
count += 1;
} else {
error!("Could not load YAML: {:?}", yaml_data);
}
}
Err(error) => {
dbg!(error);
return Err(());
}
}
}
info!("Loaded {count} conversations");
return Ok(());
}
pub fn preprocess_includes(&mut self) {
let mut include_db: HashMap<String, Vec<Value>> = HashMap::new();
for sequence in &self.0 {
if let Some(vector) = sequence.as_sequence() {
if let Some(first_item) = vector.get(0) {
if let Some(map) = first_item.as_mapping() {
for (key, value) in map {
if let (Some(key), Some(value)) = (key.as_str(), value.as_str()) {
if key == TOKEN_CHAT {
include_db.insert(value.to_string(), vector.clone());
}
}
}
}
}
}
}
for mut sequence in self.0.iter_mut() {
ChatDB::preprocess_includes_recursively(&mut sequence, &include_db);
}
}
fn preprocess_includes_recursively(
sequence: &mut Value,
include_db: &HashMap<String, Vec<Value>>,
) {
if let Some(vector) = sequence.as_sequence_mut() {
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(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 {
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);
}
}
}
_ => {}
}
index += 1;
}
}
}
pub fn get_chat_by_id(&self, id: &String) -> Result<usize, String> {
let mut found: Option<usize> = None;
for (index, object_yaml) in self.0.iter().enumerate() {
if let Some(chat_id) = object_yaml[0][TOKEN_CHAT].as_str() {
if chat_id == id {
if found.is_some() {
return Err("Found multiple chats with the same id!".to_string());
}
found = Some(index);
}
}
}
if let Some(result) = found {
return Ok(result);
}
return Err(format!(
"No chat with the conversation ID `{id}` was found."
));
}
// For a given Value, check whether it's a Value::Mapping and whether it
// contains a choice. Mappings that will be detected as choices:
// - `{"What's up?": [...]}`
// - `{"What's up?": [...], "if": "value > 3"}`
// Not acceptable:
// - `"What's up?"`
// - `{"goto": "foo"}`
fn is_choice(&self, yaml: Option<&Value>) -> bool {
if let Some(data) = self.extract_choice(yaml) {
return data.choice_text.is_some();
}
return false;
}
fn extract_choice(&self, yaml: Option<&Value>) -> Option<Extracted> {
let non_choice_tokens = NON_CHOICE_TOKENS.to_vec();
if let Some(Value::Mapping(map)) = yaml {
let mut result: Extracted = Extracted::default();
for (key, value) in map {
if let Value::String(key) = key {
if key == TOKEN_NOWAIT && value.as_bool() == Some(true) {
result.nowait = true;
} else if key == TOKEN_IF {
if let Some(condition) = value.as_str() {
result.condition = Some(condition.to_string());
}
} else if non_choice_tokens.contains(&key.as_str()) {
// skip over the other non-choice tokens
} else if key.as_str().starts_with(TOKEN_IF_INLINE) {
// skip over inlined if-statements
} else {
result.choice_text = Some(key.to_string());
}
}
}
return Some(result);
}
return None;
}
fn search_label_recursively(
&self,
sequence: &Value,
label: &String,
mut pos: ChatPos,
) -> Option<ChatPos> {
if pos.len() > MAX_BRANCH_DEPTH {
return None;
}
if let Some(vector) = sequence.as_sequence() {
for (index, item) in vector.iter().enumerate() {
match item {
Value::String(_) => {}
Value::Mapping(map) => {
for (key, value) in map {
if let Some(key) = key.as_str() {
if key == TOKEN_LABEL {
if value == label {
pos.push(index);
return Some(pos);
}
}
}
if value.is_sequence() {
pos.push(index);
if let Some(result) =
self.search_label_recursively(value, label, pos.clone())
{
return Some(result);
}
pos.pop();
}
}
}
_ => {}
}
}
}
return None;
}
fn search_label(&self, chat_id: usize, label: &String) -> Option<ChatPos> {
if label == TOKEN_GOTO_EXIT {
return Some(vec![]);
}
return self.search_label_recursively(&self.0[chat_id], label, vec![]);
}
// returns true if we reached the end of a branch and possibly popped the position stack
fn advance_pointer(&self, chat: &mut Chat) -> bool {
let len = chat.position.len();
if len == 0 {
return true; // out of bounds
}
chat.position[len - 1] += 1;
let mut popped = false;
let mut seek_past_dialog_choices = false;
while chat.position.len() > 0 {
match self.at(chat.internal_id, &chat.position) {
None => {
chat.position.pop();
popped = true;
seek_past_dialog_choices = true;
if chat.position.len() > 0 {
let index = chat.position.len() - 1;
chat.position[index] += 1;
}
}
Some(Value::Mapping(map)) => {
if seek_past_dialog_choices && self.is_choice(Some(&Value::Mapping(map))) {
// we just dropped out of a branch and ended up in a dialog
// choice. let's seek past all the choices until we find
// the next non-dialog-choice item.
if chat.position.len() > 0 {
let index = chat.position.len() - 1;
chat.position[index] += 1;
}
} else {
break;
}
}
Some(_) => {
break;
}
}
}
if chat.position.len() == 0 {
return true; // out of bounds
}
return popped;
}
// Returns the Value at the given ID/position, as-is.
// If it's a choice, it returns the entire {"choice text": [...]} mapping.
fn at(&self, id: usize, position: &Vec<usize>) -> Option<Value> {
if position.len() == 0 {
return None;
}
let mut result: Option<Value> = None;
let mut pointer: Option<Value> = Some(self.0[id].clone());
let mut next_pointer: Option<Value> = None;
for index in position {
if let Some(Value::Sequence(seq)) = &pointer {
let value = seq.get(*index);
match value {
Some(Value::String(value_string)) => {
result = Some(Value::String(value_string.into()));
}
Some(Value::Mapping(mapping)) => {
result = Some(Value::Mapping(mapping.clone()));
for value in mapping.values() {
if let Some(list) = value.as_sequence() {
next_pointer = Some(Value::Sequence(list.clone()));
break;
}
}
}
None => {
return None; // Out of bounds.
}
_ => {
error!("Could not handle YAML value {value:?}");
return None;
}
}
pointer = next_pointer;
next_pointer = None;
} else {
return None;
}
}
return result;
}
// Determines whether the item at the current position is "skippable".
// This means that we should process it right away to get to the correct
// position for finding the choices of a message.
// This includes flow control tokens like "goto", no-op tokens like "label",
// but not something with a side effect like "script", and especially not "if".
fn is_skippable(&self, chat: &mut Chat) -> bool {
let current_item = self.at(chat.internal_id, &chat.position);
if current_item.is_none() {
return false;
}
if let Some(map) = current_item.unwrap().as_mapping() {
for key in map.keys() {
if let Some(key) = key.as_str() {
if !SKIPPABLE_TOKENS.contains(&key) {
return false;
}
} else {
return false;
}
}
// It's a mapping that contains ONLY keys in SKIPPABLE_TOKENS.
return true;
}
return false;
}
fn process_yaml_entry(&self, chat: &mut Chat, event: &mut EventWriter<ChatEvent>) -> bool {
let current_item = self.at(chat.internal_id, &chat.position);
let mut processed_a_choice = false;
match current_item {
Some(Value::String(message)) => {
event.send(ChatEvent::SpawnMessage(
message.to_string(),
hud::LogLevel::Chat,
DEFAULT_SOUND.to_string(),
));
}
Some(Value::Mapping(map)) => {
let mut sound = DEFAULT_SOUND.to_string();
// Is this a dialog choice?
if self.is_choice(Some(&Value::Mapping(map.clone()))) {
processed_a_choice = true;
}
// We're going through the list of keys/values multiple times
// to ensure that dependencies for certain commands are available
// First pass
for (key, value) in &map {
let key = key.as_str();
match (key, value) {
(Some(TOKEN_IF), Value::String(condition)) => {
let mut pos = chat.position.clone();
pos.push(0);
event.send(ChatEvent::GotoIf(condition.into(), pos));
}
(Some(TOKEN_SOUND), Value::String(sound_name)) => {
sound = sound_name.clone();
}
_ => {}
}
if let Some(key) = key {
if key.starts_with(TOKEN_IF_INLINE) {
let condition: &str = &key[TOKEN_IF_INLINE.len()..];
let mut pos = chat.position.clone();
pos.push(0);
event.send(ChatEvent::GotoIf(condition.into(), pos));
}
}
}
// Second pass
for (key, value) in &map {
let key = key.as_str();
match (key, value) {
(Some(TOKEN_MSG), Value::String(message)) => {
event.send(ChatEvent::SpawnMessage(
message.to_string(),
hud::LogLevel::Chat,
sound.clone(),
));
}
(Some(TOKEN_SYSTEM), Value::String(message)) => {
event.send(ChatEvent::SpawnMessage(
message.to_string(),
hud::LogLevel::Info,
sound.clone(),
));
}
(Some(TOKEN_WARN), Value::String(message)) => {
event.send(ChatEvent::SpawnMessage(
message.to_string(),
hud::LogLevel::Warning,
sound.clone(),
));
}
(Some(TOKEN_SET), Value::String(instructions)) => {
event.send(ChatEvent::SetVariable(instructions.to_string()));
}
_ => {}
}
}
// Third pass
for (key, value) in &map {
let key = key.as_str();
match (key, value) {
(Some(TOKEN_SLEEP), Value::Number(time)) => {
if let Some(time_f64) = time.as_f64() {
event.send(ChatEvent::SleepSeconds(time_f64));
}
}
(Some(TOKEN_GOTO), Value::String(label)) => {
match self.search_label(chat.internal_id, &label) {
Some(pos) => {
chat.position = pos;
}
None => {
error!("Could not find goto label {label}!");
}
}
}
(Some(TOKEN_SCRIPT), Value::String(script)) => {
event.send(ChatEvent::RunScript(script.clone()));
}
_ => {}
}
}
}
None => {
if chat.position.len() == 0 {
event.send(ChatEvent::SpawnMessage(
"Disconnected.".to_string(),
hud::LogLevel::Info,
DEFAULT_SOUND.to_string(),
));
event.send(ChatEvent::DespawnAllChats);
}
}
_ => {
error!("Can't handle YAML value {current_item:?}");
}
}
return processed_a_choice;
}
pub fn advance_chat(&self, chat: &mut Chat, event: &mut EventWriter<ChatEvent>) {
event.send(ChatEvent::DespawnAllChoices);
// Handle this entry in the chat list
let processed_a_choice: bool = self.process_yaml_entry(chat, event);
// Move on to next entry
self.advance_pointer(chat);
// Add the following choices, unless we ended up in the middle of a dialog
// choice list, and should just skip through to the next non-dialog-choice item
if !processed_a_choice {
// Skip/process some entries right away, to be able to fetch the correct choices
while self.is_skippable(chat) {
self.process_yaml_entry(chat, event);
self.advance_pointer(chat);
}
// Spawn choices until we reach a non-choice item or the end of the branch
let mut key: usize = 0;
let mut reached_end_of_branch = false;
while let Some(data) =
self.extract_choice(self.at(chat.internal_id, &chat.position).as_ref())
{
if let Some(choice_text) = data.choice_text {
if reached_end_of_branch {
break;
}
let mut goto: Vec<usize> = chat.position.clone();
goto.push(0);
event.send(ChatEvent::SpawnChoice(
choice_text,
key,
goto,
data.nowait,
data.condition,
));
key += 1;
reached_end_of_branch = self.advance_pointer(chat);
} else {
break;
}
}
}
}
}
pub fn load_chats(mut chatdb: ResMut<ChatDB>) {
for chat_yaml in CHATS {
if chatdb.load_from_str(chat_yaml).is_err() {
error!("Could not load chat definitions. Validate files in `src/chats/` path.");
}
}
chatdb.preprocess_includes();
}
pub fn handle_new_conversations(
mut commands: Commands,
mut er_conv: EventReader<StartConversationEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatevent: EventWriter<ChatEvent>,
mut ew_achievement: EventWriter<game::AchievementEvent>,
chatdb: Res<ChatDB>,
q_chats: Query<&Chat>,
time: Res<Time>,
) {
for event in er_conv.read() {
if !q_chats.is_empty() {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Ping));
return;
}
match (*chatdb).get_chat_by_id(&event.talker.chat_name) {
Ok(chat_id) => {
if let Some(name) = &event.talker.name {
ew_achievement.send(game::AchievementEvent::TalkTo(name.clone()));
}
let mut chat = Chat {
internal_id: chat_id,
position: vec![0],
timer: time.elapsed_seconds_f64(),
talker: event.talker.clone(),
};
chatdb.advance_chat(&mut chat, &mut ew_chatevent);
commands.spawn((chat, world::DespawnOnPlayerDeath));
}
Err(error) => {
error!("Error while looking for chat ID: {error}");
}
}
}
}
pub fn handle_chat_timer(
time: Res<Time>,
chatdb: Res<ChatDB>,
mut q_chats: Query<&mut Chat>,
mut ew_chatevent: EventWriter<ChatEvent>,
) {
let now = time.elapsed_seconds_f64();
for mut chat in &mut q_chats {
if now >= chat.timer {
chatdb.advance_chat(&mut chat, &mut ew_chatevent);
}
}
}
pub fn handle_chat_events(
mut commands: Commands,
mut er_chatevent: EventReader<ChatEvent>,
mut ew_chatscript: EventWriter<ChatScriptEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut log: ResMut<hud::Log>,
mut vars: ResMut<var::GameVars>,
q_choices: Query<Entity, With<Choice>>,
mut q_chats: Query<(Entity, &mut Chat)>,
time: Res<Time>,
settings: Res<var::Settings>,
) {
let mut choice_key = q_choices.iter().len();
for event in er_chatevent.read() {
let now = time.elapsed_seconds_f64();
let chat_maybe = q_chats.get_single_mut();
if chat_maybe.is_err() {
return;
}
let (chat_entity, mut chat) = chat_maybe.unwrap();
match event {
ChatEvent::DespawnAllChoices => {
for entity in &q_choices {
commands.entity(entity).despawn();
}
choice_key = 0;
}
ChatEvent::DespawnAllChats => {
commands.entity(chat_entity).despawn();
}
ChatEvent::SpawnMessage(message, level, sound) => {
match level {
hud::LogLevel::Chat => {
log.chat(
message.into(),
chat.talker.name.clone().unwrap_or("".to_string()),
);
}
hud::LogLevel::Info => {
log.info(message.into());
}
hud::LogLevel::Achievement => {
log.add(message.into(), "".into(), hud::LogLevel::Achievement);
}
hud::LogLevel::Warning => {
log.warning(message.into());
}
hud::LogLevel::Always => {
log.add(
message.into(),
chat.talker.name.clone().unwrap_or("".to_string()),
hud::LogLevel::Always,
);
}
}
chat.timer = now
+ ((message.len() as f32).max(CHAT_SPEED_MIN_LEN)
* TALKER_SPEED_FACTOR
* chat.talker.talking_speed
/ settings.chat_speed) as f64;
let sfx = audio::str2sfx(sound);
ew_sfx.send(audio::PlaySfxEvent(sfx));
}
ChatEvent::SpawnChoice(replytext, _key, goto, nowait, condition) => 'out: {
if let Some(condition) = condition {
if !vars.evaluate_condition(condition, &chat.talker.actor_id) {
break 'out;
}
}
commands.spawn((
world::DespawnOnPlayerDeath,
Choice {
text: replytext.into(),
key: choice_key,
goto: goto.clone(),
},
));
choice_key += 1;
if !nowait {
chat.timer = now + CHOICE_TIMER / settings.chat_speed as f64;
}
}
ChatEvent::RunScript(script) => {
ew_chatscript.send(ChatScriptEvent(script.clone()));
}
ChatEvent::SleepSeconds(sleep_duration) => {
chat.timer = now + sleep_duration;
}
ChatEvent::SetVariable(string) => {
if let Some((key, value)) = string.split_once(" ") {
vars.set_in_scope(&chat.talker.actor_id, key, value.into());
} else {
vars.set_in_scope(&chat.talker.actor_id, string, "".into());
}
}
ChatEvent::GotoIf(condition, goto) => {
if vars.evaluate_condition(condition, &chat.talker.actor_id) {
chat.position = goto.clone();
}
}
}
}
}
fn handle_reply_keys(
keyboard_input: Res<ButtonInput<KeyCode>>,
settings: ResMut<var::Settings>,
q_choices: Query<&Choice>,
mut q_chats: Query<&mut Chat>,
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
time: Res<Time>,
) {
let mut selected_choice: usize = 0;
'outer: for key in settings.get_reply_keys() {
if keyboard_input.just_pressed(key) {
for choice in &q_choices {
if choice.key == selected_choice {
if let Ok(mut chat) = q_chats.get_single_mut() {
evwriter_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
chat.timer = time.elapsed_seconds_f64();
chat.position = choice.goto.clone();
}
break 'outer;
}
}
}
selected_choice += 1;
}
}
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<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
let mut parts = script.0.split_whitespace();
let name = parts.next();
if name.is_none() {
error!("ChatScriptEvent should not contain a script name and its parameters, got empty String");
return;
}
let name = name.unwrap();
let param1 = parts.next().unwrap_or("");
let param2 = parts.next().unwrap_or("");
// Process the script
match name {
"refilloxygen" => {
if let Ok(mut amount) = param1.to_string().parse::<f32>() {
for (_, mut suit, _) in q_player.iter_mut() {
if param2.is_empty() {
suit.oxygen = (suit.oxygen + amount).clamp(0.0, suit.oxygen_max);
} else {
let mut found_other = false;
info!("param2={}", 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 == 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 `{}`", param2);
}
}
}
} else {
error!("Invalid parameter for command `{}`: `{}`", name, param1);
}
}
"repairsuit" => {
ew_achievement.send(game::AchievementEvent::RepairSuit);
for (_, mut suit, _) in q_player.iter_mut() {
suit.integrity = 1.0;
}
}
"cryotrip" => {
if param1.is_empty() {
error!("Chat script cryotrip needs a parameter");
} else {
if let Ok((mut pos, mut v)) = q_playercam.get_single_mut() {
let busstop = match param1 {
"serenity" => Some("busstopclippy"),
"farview" => Some("busstopclippy2"),
"metisprime" => Some("busstopclippy3"),
_ => None,
};
if let Some(station) = busstop {
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);
}
}
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(visual::SpawnEffectEvent {
class: visual::Effects::FadeIn(css::AQUA.into()),
duration: 1.0,
});
}
}
"cryofadeout" => {
ew_effect.send(visual::SpawnEffectEvent {
class: visual::Effects::FadeOut(css::AQUA.into()),
duration: 5.1,
});
}
"drinkpizza" => {
ew_achievement.send(game::AchievementEvent::DrinkPizza);
}
_ => {
error!("Error, undefined chat script {name}");
}
}
}
}
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(),
);
vars.set_in_scope(
"$",
"player_suit_health_percent",
((suit.integrity * 100.0).round() as u8).to_string(),
);
}
}