outfly/src/chat.rs

422 lines
14 KiB
Rust
Raw Normal View History

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-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";
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;
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,
];
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-13 16:57:23 +00:00
handle_reply_keys.before(handle_chat_timer),
handle_chat_timer.before(handle_chat_events),
handle_new_conversations.before(handle_chat_events),
handle_chat_events,
2024-04-12 21:03:46 +00:00
));
app.add_event::<StartConversationEvent>();
app.add_event::<ChatEvent>();
2024-04-12 19:34:55 +00:00
app.insert_resource(ChatDB(Vec::new()));
}
}
2024-04-13 16:57:23 +00:00
type ChatPos = Vec<usize>;
#[derive(Component)]
pub struct Chat {
pub id: usize,
2024-04-13 16:57:23 +00:00
pub position: ChatPos,
pub timer: f64,
pub talker: Talker,
}
#[derive(Component)]
2024-04-13 13:26:45 +00:00
pub struct Choice {
pub text: String,
pub key: usize,
2024-04-13 16:57:23 +00:00
pub goto: ChatPos,
2024-04-13 13:26:45 +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
}
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());
}
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 18:23:38 +00:00
// 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 search_choice(&self, yaml: Option<&Value>) -> Option<(String, Value)> {
2024-04-13 10:24:56 +00:00
let non_choice_tokens = NON_CHOICE_TOKENS.to_vec();
2024-04-13 19:07:51 +00:00
if let Some(Value::Mapping(map)) = yaml {
for key in map.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;
}
2024-04-13 19:07:51 +00:00
return Some((key.into(), map[key].clone()));
2024-04-13 10:24:56 +00:00
}
}
}
return None;
}
// returns true if we reached the end of a branch and possibly popped the position stack
2024-04-13 14:03:15 +00:00
fn advance_pointer(&self, chat: &mut Chat) -> bool {
2024-04-13 18:23:38 +00:00
let len = chat.position.len();
if len == 0 {
return true; // out of bounds
2024-04-13 18:23:38 +00:00
}
chat.position[len - 1] += 1;
let mut popped = false;
while chat.position.len() > 0 {
2024-04-13 18:23:38 +00:00
dbg!(&chat.position);
2024-04-13 16:57:23 +00:00
dbg!(self.at(chat.id, &chat.position));
match self.at(chat.id, &chat.position) {
None => {
dbg!("Pop.");
chat.position.pop();
popped = true;
2024-04-13 18:23:38 +00:00
if chat.position.len() > 0 {
let index = chat.position.len() - 1;
chat.position[index] += 1;
}
2024-04-13 16:57:23 +00:00
},
Some(_) => {
break;
}
}
}
2024-04-13 18:23:38 +00:00
if chat.position.len() == 0 {
return true; // out of bounds
2024-04-13 18:23:38 +00:00
}
return popped;
2024-04-13 14:03:15 +00:00
}
2024-04-13 18:23:38 +00:00
// 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;
2024-04-13 15:17:24 +00:00
}
2024-04-13 18:23:38 +00:00
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 {
2024-04-13 18:23:38 +00:00
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)) => {
if let Some((_choicetext, subconversation)) = self.search_choice(value) {
result = Some(Value::Mapping(mapping.clone()));
next_pointer = Some(subconversation);
}
else {
result = Some(Value::Mapping(mapping.clone()));
}
}
None => {
return None; // Out of bounds.
2024-04-13 18:23:38 +00:00
}
_ => {
error!("Could not handle YAML value {value:?}");
return None;
}
2024-04-13 16:57:23 +00:00
}
2024-04-13 18:23:38 +00:00
pointer = next_pointer;
next_pointer = None;
} else {
return None;
}
}
2024-04-13 18:23:38 +00:00
return result;
2024-04-13 14:03:15 +00:00
}
2024-04-13 19:07:51 +00:00
pub fn process_yaml_entry(
&self,
chat: &mut Chat,
event: &mut EventWriter<ChatEvent>,
) -> bool {
let current_item = self.at(chat.id, &chat.position);
let mut add_choices = true;
match current_item {
Some(Value::String(message)) => {
event.send(ChatEvent::SpawnMessage(message.to_string()));
}
Some(Value::Mapping(message)) => {
if let Some(_) = self.search_choice(Some(&Value::Mapping(message))) {
add_choices = false;
}
}
None => {
2024-04-13 19:07:51 +00:00
if chat.position.len() == 0 {
event.send(ChatEvent::SpawnMessage("Disconnected.".to_string()));
event.send(ChatEvent::DespawnAllChats);
}
}
_ => {
error!("Can't handle YAML value {current_item:?}");
}
}
2024-04-13 19:07:51 +00:00
return add_choices;
}
pub fn advance_chat(&self, chat: &mut Chat, event: &mut EventWriter<ChatEvent>) {
event.send(ChatEvent::DespawnAllChoices);
// Handle this entry in the chat list
let add_choices = self.process_yaml_entry(chat, event);
2024-04-13 10:24:56 +00:00
// Move on to next entry
let mut finished_branch = self.advance_pointer(chat);
// Add choices, if available
if add_choices {
let mut key: usize = 0;
while let Some((choice, _)) =
self.search_choice(self.at(chat.id, &chat.position).as_ref()) {
if finished_branch {
break;
}
let mut goto: Vec<usize> = chat.position.clone();
2024-04-13 16:57:23 +00:00
goto.push(0);
event.send(ChatEvent::SpawnChoice(choice, key, goto));
2024-04-13 13:26:45 +00:00
key += 1;
finished_branch = self.advance_pointer(chat);
2024-04-13 10:24:56 +00:00
}
}
}
2024-04-12 21:03:46 +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>,
pub talking_speed: f32,
}
#[derive(Event)]
pub struct StartConversationEvent {
pub talker: Talker,
}
2024-04-12 19:26:23 +00:00
#[derive(Event)]
pub enum ChatEvent {
DespawnAllChoices,
DespawnAllChats,
2024-04-13 13:26:45 +00:00
SpawnMessage(String),
2024-04-13 16:57:23 +00:00
SpawnChoice(String, usize, ChatPos),
//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(
mut commands: Commands,
2024-04-12 21:03:46 +00:00
mut er_conv: EventReader<StartConversationEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_chatevent: EventWriter<ChatEvent>,
2024-04-12 21:03:46 +00:00
chatdb: Res<ChatDB>,
q_chats: Query<&Chat>,
time: Res<Time>,
2024-04-12 21:03:46 +00:00
) {
for event in er_conv.read() {
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));
let mut chat = Chat {
id: chat_id,
2024-04-13 14:03:15 +00:00
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,
));
2024-04-12 21:03:46 +00:00
}
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 log: ResMut<hud::Log>,
q_choices: Query<Entity, With<Choice>>,
mut q_chats: Query<(Entity, &mut Chat)>,
time: Res<Time>,
2024-04-12 23:34:18 +00:00
settings: Res<settings::Settings>,
) {
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();
}
}
ChatEvent::DespawnAllChats => {
commands.entity(chat_entity).despawn();
}
2024-04-13 13:26:45 +00:00
ChatEvent::SpawnMessage(message) => {
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-13 16:57:23 +00:00
ChatEvent::SpawnChoice(replytext, key, goto) => {
2024-04-13 13:26:45 +00:00
commands.spawn((
world::DespawnOnPlayerDeath,
Choice {
text: replytext.into(),
key: *key,
2024-04-13 16:57:23 +00:00
goto: goto.clone(),
2024-04-13 13:26:45 +00:00
}
));
chat.timer = now + CHOICE_TIMER / settings.chat_speed as f64;
2024-04-13 13:26:45 +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>,
2024-04-13 16:57:23 +00:00
mut q_chats: Query<&mut Chat>,
2024-04-13 13:44:23 +00:00
//mut evwriter_sendmsg: EventWriter<SendMessageEvent>,
mut evwriter_sfx: EventWriter<audio::PlaySfxEvent>,
2024-04-13 16:57:23 +00:00
time: Res<Time>,
2024-04-13 13:44:23 +00:00
) {
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 {
2024-04-13 16:57:23 +00:00
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();
info!("GOTO {:?}", &chat.position);
}
2024-04-13 13:44:23 +00:00
break 'outer;
}
}
}
selected_choice += 1;
}
}