2024-04-04 11:33:54 +00:00
|
|
|
use bevy::prelude::*;
|
2024-04-13 15:17:24 +00:00
|
|
|
use serde_yaml::Value;
|
|
|
|
use serde::Deserialize;
|
2024-04-12 23:34:18 +00:00
|
|
|
use crate::{audio, hud, settings, world};
|
2024-04-04 11:33:54 +00:00
|
|
|
|
2024-04-12 19:34:55 +00:00
|
|
|
pub const CHATS: &[&str] = &[
|
|
|
|
include_str!("chats/serenity.yaml"),
|
|
|
|
include_str!("chats/startrans.yaml"),
|
|
|
|
];
|
|
|
|
|
2024-04-12 21:03:46 +00:00
|
|
|
pub const TOKEN_CHAT: &str = "chat";
|
2024-04-13 10:24:56 +00:00
|
|
|
pub const TOKEN_SYSTEM: &str = "system";
|
|
|
|
pub const TOKEN_WARN: &str = "warn";
|
|
|
|
pub const TOKEN_SET: &str = "set";
|
|
|
|
pub const TOKEN_IF: &str = "if";
|
|
|
|
pub const TOKEN_GOTO: &str = "goto";
|
|
|
|
pub const TOKEN_LABEL: &str = "label";
|
|
|
|
pub const TOKEN_SCRIPT: &str = "script";
|
2024-04-13 13:34:35 +00:00
|
|
|
|
|
|
|
pub const NAME_FALLBACK: &str = "Unknown";
|
|
|
|
|
|
|
|
pub const CHOICE_TIMER: f64 = 40.0 * settings::DEFAULT_CHAT_SPEED as f64;
|
2024-04-12 23:34:18 +00:00
|
|
|
pub const LETTERS_PER_SECOND: f32 = 17.0;
|
|
|
|
pub const TALKER_SPEED_FACTOR: f32 = settings::DEFAULT_CHAT_SPEED / LETTERS_PER_SECOND;
|
2024-04-12 23:21:38 +00:00
|
|
|
pub const CHAT_SPEED_MIN_LEN: f32 = 40.0;
|
2024-04-12 21:03:46 +00:00
|
|
|
|
2024-04-13 10:24:56 +00:00
|
|
|
pub const NON_CHOICE_TOKENS: &[&str] = &[
|
|
|
|
TOKEN_CHAT,
|
|
|
|
TOKEN_SYSTEM,
|
|
|
|
TOKEN_WARN,
|
|
|
|
TOKEN_SET,
|
|
|
|
TOKEN_IF,
|
|
|
|
TOKEN_GOTO,
|
|
|
|
TOKEN_LABEL,
|
|
|
|
TOKEN_SCRIPT,
|
|
|
|
];
|
|
|
|
|
2024-04-04 11:33:54 +00:00
|
|
|
pub struct ChatPlugin;
|
|
|
|
impl Plugin for ChatPlugin {
|
|
|
|
fn build(&self, app: &mut App) {
|
2024-04-12 19:26:23 +00:00
|
|
|
app.add_systems(Startup, load_chats);
|
2024-04-12 21:03:46 +00:00
|
|
|
app.add_systems(Update, (
|
2024-04-12 23:21:38 +00:00
|
|
|
handle_new_conversations.before(handle_chat_events),
|
|
|
|
handle_chat_events,
|
2024-04-13 13:44:23 +00:00
|
|
|
handle_reply_keys,
|
2024-04-12 23:21:38 +00:00
|
|
|
handle_chat_timer.before(handle_chat_events),
|
2024-04-12 21:03:46 +00:00
|
|
|
));
|
2024-04-04 11:33:54 +00:00
|
|
|
app.add_event::<StartConversationEvent>();
|
2024-04-12 22:05:42 +00:00
|
|
|
app.add_event::<ChatEvent>();
|
2024-04-12 19:34:55 +00:00
|
|
|
app.insert_resource(ChatDB(Vec::new()));
|
2024-04-04 11:33:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-12 21:28:15 +00:00
|
|
|
#[derive(Component)]
|
|
|
|
pub struct Chat {
|
2024-04-12 22:39:21 +00:00
|
|
|
pub id: usize,
|
2024-04-13 14:03:15 +00:00
|
|
|
pub position: Vec<usize>,
|
2024-04-12 21:28:15 +00:00
|
|
|
pub timer: f64,
|
2024-04-12 23:21:38 +00:00
|
|
|
pub talker: Talker,
|
2024-04-12 21:28:15 +00:00
|
|
|
}
|
|
|
|
|
2024-04-12 22:05:42 +00:00
|
|
|
#[derive(Component)]
|
2024-04-13 13:26:45 +00:00
|
|
|
pub struct Choice {
|
|
|
|
pub text: String,
|
|
|
|
pub key: usize,
|
|
|
|
}
|
2024-04-12 22:05:42 +00:00
|
|
|
|
2024-04-12 22:11:32 +00:00
|
|
|
// This is the only place where any YAML interaction should be happening.
|
2024-04-12 21:03:46 +00:00
|
|
|
#[derive(Resource)]
|
2024-04-13 15:17:24 +00:00
|
|
|
pub struct ChatDB(Vec<Value>);
|
2024-04-12 21:03:46 +00:00
|
|
|
impl ChatDB {
|
2024-04-12 22:11:32 +00:00
|
|
|
pub fn load_from_str(&mut self, yaml_string: &str) -> Result<(), ()> {
|
2024-04-13 15:17:24 +00:00
|
|
|
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(());
|
|
|
|
}
|
|
|
|
}
|
2024-04-12 22:11:32 +00:00
|
|
|
}
|
2024-04-13 15:17:24 +00:00
|
|
|
info!("Loaded {count} conversations");
|
|
|
|
return Ok(());
|
2024-04-12 22:11:32 +00:00
|
|
|
}
|
|
|
|
|
2024-04-12 22:39:21 +00:00
|
|
|
pub fn get_chat_by_id(&self, id: &String) -> Result<usize, String> {
|
|
|
|
let mut found: Option<usize> = None;
|
2024-04-12 21:03:46 +00:00
|
|
|
for (index, object_yaml) in self.0.iter().enumerate() {
|
2024-04-12 21:18:07 +00:00
|
|
|
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());
|
|
|
|
}
|
2024-04-12 22:39:21 +00:00
|
|
|
found = Some(index);
|
2024-04-12 21:18:07 +00:00
|
|
|
}
|
2024-04-12 21:03:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(result) = found {
|
|
|
|
return Ok(result);
|
|
|
|
}
|
|
|
|
return Err(format!("No chat with the conversation ID `{id}` was found."));
|
|
|
|
}
|
2024-04-12 22:11:32 +00:00
|
|
|
|
2024-04-13 15:17:24 +00:00
|
|
|
fn search_choice(&self, yaml: Option<&Value>) -> Option<String> {
|
2024-04-13 10:24:56 +00:00
|
|
|
let non_choice_tokens = NON_CHOICE_TOKENS.to_vec();
|
2024-04-13 15:17:24 +00:00
|
|
|
if let Some(Value::Mapping(hash)) = yaml {
|
2024-04-13 10:24:56 +00:00
|
|
|
for key in hash.keys() {
|
2024-04-13 15:17:24 +00:00
|
|
|
if let Value::String(key) = key {
|
2024-04-13 10:24:56 +00:00
|
|
|
if non_choice_tokens.contains(&key.as_str()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return Some(key.into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
2024-04-13 14:03:15 +00:00
|
|
|
// returns false if the advanced pointer is out of bounds
|
|
|
|
fn advance_pointer(&self, chat: &mut Chat) -> bool {
|
|
|
|
chat.position[0] += 1;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn pointer_lookahead(&self, position: &mut Vec<usize>) {
|
|
|
|
let index = position.len() - 1;
|
|
|
|
position[index] += 1;
|
|
|
|
}
|
|
|
|
|
2024-04-13 15:17:24 +00:00
|
|
|
fn at(&self, id: usize, position: &Vec<usize>) -> Option<&Value> {
|
|
|
|
if let Some(Value::Sequence(seq)) = self.0.get(id) {
|
|
|
|
return Some(&seq[position[0]]); // TODO: panic
|
|
|
|
}
|
2024-04-13 14:03:15 +00:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
2024-04-12 22:05:42 +00:00
|
|
|
pub fn advance_chat(&self, chat: &mut Chat, event: &mut EventWriter<ChatEvent>) {
|
|
|
|
event.send(ChatEvent::DespawnAllChoices);
|
2024-04-13 15:17:24 +00:00
|
|
|
let conv = &self.0.get(chat.id);
|
2024-04-12 23:21:38 +00:00
|
|
|
if conv.is_none() {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let conv = conv.unwrap();
|
2024-04-13 10:24:56 +00:00
|
|
|
|
|
|
|
// Handle next entry in the chat list
|
|
|
|
let mut is_skipping_through = false;
|
2024-04-13 14:03:15 +00:00
|
|
|
if !self.advance_pointer(chat) {
|
2024-04-13 13:26:45 +00:00
|
|
|
event.send(ChatEvent::SpawnMessage("Disconnected.".to_string()));
|
2024-04-12 23:21:38 +00:00
|
|
|
event.send(ChatEvent::DespawnAllChats);
|
|
|
|
return;
|
|
|
|
}
|
2024-04-13 15:17:24 +00:00
|
|
|
else if let Some(_) = self.search_choice(self.at(chat.id, &chat.position)) {
|
2024-04-13 10:24:56 +00:00
|
|
|
is_skipping_through = true;
|
|
|
|
}
|
2024-04-13 14:03:15 +00:00
|
|
|
else if let Some(message) = conv[chat.position[0]].as_str() {
|
2024-04-13 13:26:45 +00:00
|
|
|
event.send(ChatEvent::SpawnMessage(message.to_string()));
|
2024-04-12 22:39:21 +00:00
|
|
|
}
|
2024-04-13 10:24:56 +00:00
|
|
|
|
|
|
|
// Check if the immediately following entries are choices
|
2024-04-13 14:03:15 +00:00
|
|
|
let mut pos = chat.position.clone();
|
|
|
|
self.pointer_lookahead(&mut pos);
|
2024-04-13 13:26:45 +00:00
|
|
|
let mut key: usize = 0;
|
2024-04-13 10:24:56 +00:00
|
|
|
loop {
|
2024-04-13 15:17:24 +00:00
|
|
|
if is_skipping_through /*|| pos[0] >= conv.len()*/ { // TODO: out of bounds checking
|
2024-04-13 10:24:56 +00:00
|
|
|
break;
|
|
|
|
}
|
2024-04-13 15:17:24 +00:00
|
|
|
if let Some(choice) = self.search_choice(self.at(chat.id, &pos)) {
|
2024-04-13 13:26:45 +00:00
|
|
|
event.send(ChatEvent::SpawnChoice(choice, key));
|
|
|
|
key += 1;
|
2024-04-13 10:24:56 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
break;
|
|
|
|
}
|
2024-04-13 14:03:15 +00:00
|
|
|
self.pointer_lookahead(&mut pos);
|
2024-04-13 10:24:56 +00:00
|
|
|
}
|
2024-04-12 22:05:42 +00:00
|
|
|
}
|
2024-04-12 21:03:46 +00:00
|
|
|
}
|
|
|
|
|
2024-04-04 11:33:54 +00:00
|
|
|
#[derive(Component)]
|
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Talker {
|
|
|
|
pub conv_id: String,
|
2024-04-12 21:03:46 +00:00
|
|
|
pub name: Option<String>,
|
|
|
|
pub pronoun: Option<String>,
|
2024-04-12 23:21:38 +00:00
|
|
|
pub talking_speed: f32,
|
2024-04-04 11:33:54 +00:00
|
|
|
}
|
|
|
|
|
2024-04-12 18:39:10 +00:00
|
|
|
#[derive(Event)]
|
|
|
|
pub struct StartConversationEvent {
|
|
|
|
pub talker: Talker,
|
2024-04-04 11:39:49 +00:00
|
|
|
}
|
2024-04-12 19:26:23 +00:00
|
|
|
|
2024-04-12 22:05:42 +00:00
|
|
|
#[derive(Event)]
|
|
|
|
pub enum ChatEvent {
|
|
|
|
DespawnAllChoices,
|
2024-04-12 23:21:38 +00:00
|
|
|
DespawnAllChats,
|
2024-04-13 13:26:45 +00:00
|
|
|
SpawnMessage(String),
|
|
|
|
SpawnChoice(String, usize),
|
2024-04-12 22:05:42 +00:00
|
|
|
//Script(String, String, String),
|
|
|
|
}
|
|
|
|
|
2024-04-12 19:34:55 +00:00
|
|
|
pub fn load_chats(mut chatdb: ResMut<ChatDB>) {
|
|
|
|
for chat_yaml in CHATS {
|
2024-04-12 22:11:32 +00:00
|
|
|
if chatdb.load_from_str(chat_yaml).is_err() {
|
2024-04-12 19:34:55 +00:00
|
|
|
error!("Could not load chat definitions. Validate files in `src/chats/` path.");
|
|
|
|
}
|
2024-04-12 19:26:23 +00:00
|
|
|
}
|
|
|
|
}
|
2024-04-12 21:03:46 +00:00
|
|
|
|
|
|
|
pub fn handle_new_conversations(
|
2024-04-12 21:28:15 +00:00
|
|
|
mut commands: Commands,
|
2024-04-12 21:03:46 +00:00
|
|
|
mut er_conv: EventReader<StartConversationEvent>,
|
|
|
|
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
|
2024-04-12 22:05:42 +00:00
|
|
|
mut ew_chatevent: EventWriter<ChatEvent>,
|
2024-04-12 21:03:46 +00:00
|
|
|
chatdb: Res<ChatDB>,
|
2024-04-12 21:28:15 +00:00
|
|
|
q_chats: Query<&Chat>,
|
|
|
|
time: Res<Time>,
|
2024-04-12 21:03:46 +00:00
|
|
|
) {
|
|
|
|
for event in er_conv.read() {
|
2024-04-12 22:05:42 +00:00
|
|
|
if !q_chats.is_empty() {
|
|
|
|
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Ping));
|
|
|
|
return;
|
|
|
|
}
|
2024-04-12 21:03:46 +00:00
|
|
|
match (*chatdb).get_chat_by_id(&event.talker.conv_id) {
|
|
|
|
Ok(chat_id) => {
|
|
|
|
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Ping));
|
2024-04-12 22:05:42 +00:00
|
|
|
let mut chat = Chat {
|
|
|
|
id: chat_id,
|
2024-04-13 14:03:15 +00:00
|
|
|
position: vec![0],
|
2024-04-12 22:05:42 +00:00
|
|
|
timer: time.elapsed_seconds_f64(),
|
2024-04-12 23:21:38 +00:00
|
|
|
talker: event.talker.clone(),
|
2024-04-12 22:05:42 +00:00
|
|
|
};
|
|
|
|
chatdb.advance_chat(&mut chat, &mut ew_chatevent);
|
2024-04-12 21:28:15 +00:00
|
|
|
commands.spawn((
|
2024-04-12 22:05:42 +00:00
|
|
|
chat,
|
2024-04-12 21:28:15 +00:00
|
|
|
world::DespawnOnPlayerDeath,
|
|
|
|
));
|
2024-04-12 21:03:46 +00:00
|
|
|
}
|
|
|
|
Err(error) => {
|
|
|
|
error!("Error while looking for chat ID: {error}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-12 22:05:42 +00:00
|
|
|
|
2024-04-12 23:21:38 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-12 22:05:42 +00:00
|
|
|
pub fn handle_chat_events(
|
|
|
|
mut commands: Commands,
|
|
|
|
mut er_chatevent: EventReader<ChatEvent>,
|
2024-04-12 22:39:21 +00:00
|
|
|
mut log: ResMut<hud::Log>,
|
2024-04-12 22:05:42 +00:00
|
|
|
q_choices: Query<Entity, With<Choice>>,
|
2024-04-12 23:21:38 +00:00
|
|
|
mut q_chats: Query<(Entity, &mut Chat)>,
|
|
|
|
time: Res<Time>,
|
2024-04-12 23:34:18 +00:00
|
|
|
settings: Res<settings::Settings>,
|
2024-04-12 22:05:42 +00:00
|
|
|
) {
|
|
|
|
for event in er_chatevent.read() {
|
2024-04-12 23:21:38 +00:00
|
|
|
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();
|
|
|
|
|
2024-04-12 22:05:42 +00:00
|
|
|
match event {
|
|
|
|
ChatEvent::DespawnAllChoices => {
|
|
|
|
for entity in &q_choices {
|
|
|
|
commands.entity(entity).despawn();
|
|
|
|
}
|
|
|
|
}
|
2024-04-12 23:21:38 +00:00
|
|
|
ChatEvent::DespawnAllChats => {
|
|
|
|
commands.entity(chat_entity).despawn();
|
|
|
|
}
|
2024-04-13 13:26:45 +00:00
|
|
|
ChatEvent::SpawnMessage(message) => {
|
2024-04-12 23:25:45 +00:00
|
|
|
log.chat(message.into(), chat.talker.name.clone().unwrap_or(NAME_FALLBACK.to_string()));
|
2024-04-12 23:34:18 +00:00
|
|
|
chat.timer = now + ((message.len() as f32).max(CHAT_SPEED_MIN_LEN) * TALKER_SPEED_FACTOR * chat.talker.talking_speed / settings.chat_speed) as f64;
|
2024-04-12 22:39:21 +00:00
|
|
|
}
|
2024-04-13 13:26:45 +00:00
|
|
|
ChatEvent::SpawnChoice(replytext, key) => {
|
|
|
|
commands.spawn((
|
|
|
|
world::DespawnOnPlayerDeath,
|
|
|
|
Choice {
|
|
|
|
text: replytext.into(),
|
|
|
|
key: *key,
|
|
|
|
}
|
|
|
|
));
|
2024-04-13 13:34:35 +00:00
|
|
|
chat.timer = now + CHOICE_TIMER / settings.chat_speed as f64;
|
2024-04-13 13:26:45 +00:00
|
|
|
}
|
2024-04-12 22:05:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-04-13 13:44:23 +00:00
|
|
|
|
|
|
|
fn handle_reply_keys(
|
|
|
|
keyboard_input: Res<ButtonInput<KeyCode>>,
|
|
|
|
settings: ResMut<settings::Settings>,
|
|
|
|
q_choices: Query<&Choice>,
|
|
|
|
//mut evwriter_sendmsg: EventWriter<SendMessageEvent>,
|
|
|
|
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
|
|
|
|
) {
|
|
|
|
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 {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
selected_choice += 1;
|
|
|
|
}
|
|
|
|
}
|