2024-04-04 11:33:54 +00:00
use bevy ::prelude ::* ;
2024-04-10 23:34:16 +00:00
use bevy_xpbd_3d ::prelude ::* ;
use bevy ::math ::DVec3 ;
2024-04-10 23:12:07 +00:00
use crate ::{ actor , audio , hud , settings , world , effects } ;
2024-04-04 11:33:54 +00:00
pub struct ChatPlugin ;
impl Plugin for ChatPlugin {
fn build ( & self , app : & mut App ) {
app . register_type ::< ChatBranch > ( ) ;
app . add_systems ( Update , (
handle_new_conversations ,
2024-04-04 16:53:20 +00:00
handle_reply_keys ,
2024-04-04 11:33:54 +00:00
handle_send_messages ,
handle_conversations ,
handle_chat_scripts ,
) ) ;
2024-04-04 11:39:49 +00:00
app . add_systems ( PostUpdate , despawn_old_choices ) ;
2024-04-04 11:33:54 +00:00
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 ,
}
2024-04-04 11:39:49 +00:00
#[ derive(Component) ]
pub struct ChoiceAvailable {
pub conv_id : String ,
pub conv_label : String ,
pub recipient : String ,
pub text : String ,
}
2024-04-04 11:33:54 +00:00
#[ 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
2024-04-05 03:10:03 +00:00
commands . spawn ( (
Chat {
id : id . to_string ( ) ,
label : label . to_string ( ) ,
timer : time . elapsed_seconds_f64 ( ) ,
} ,
world ::DespawnOnPlayerDeath ,
) ) ;
2024-04-04 11:33:54 +00:00
break ;
}
}
2024-04-04 16:53:20 +00:00
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 ;
}
}
2024-04-04 11:33:54 +00:00
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 {
2024-04-04 16:53:32 +00:00
error! ( " Expected 1 branch with ID '{}', label '{}', choice '{}', but got {}! Aborting conversation. " , event . conv_id , event . conv_label , event . text , branches . len ( ) ) ;
2024-04-04 11:33:54 +00:00
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 {
2024-04-05 00:58:02 +00:00
commands . spawn ( (
ChoiceAvailable {
conv_id : choice . id . clone ( ) ,
conv_label : choice . label . clone ( ) ,
recipient : choice . name . clone ( ) ,
text : choice . choice . clone ( ) ,
} ,
world ::DespawnOnPlayerDeath ,
) ) ;
2024-04-04 11:33:54 +00:00
}
}
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 {
2024-04-05 00:58:02 +00:00
commands . spawn ( (
ChoiceAvailable {
conv_id : choice . id . clone ( ) ,
conv_label : choice . label . clone ( ) ,
recipient : choice . name . clone ( ) ,
text : choice . choice . clone ( ) ,
} ,
world ::DespawnOnPlayerDeath ,
) ) ;
2024-04-04 11:33:54 +00:00
}
}
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 > > ,
2024-04-11 19:30:27 +00:00
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 > > ,
2024-04-10 23:12:07 +00:00
mut ew_sfx : EventWriter < audio ::PlaySfxEvent > ,
mut ew_effect : EventWriter < effects ::SpawnEffectEvent > ,
2024-04-04 11:33:54 +00:00
) {
for script in er_chatscript . read ( ) {
match script . name . as_str ( ) {
" refilloxygen " = > if let Ok ( mut amount ) = script . param . parse ::< f32 > ( ) {
2024-04-11 19:30:27 +00:00
for ( _ , mut suit , _ ) in q_player . iter_mut ( ) {
2024-04-04 11:33:54 +00:00
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 ) ;
}
2024-04-10 23:12:07 +00:00
" cryotrip " = > {
2024-04-10 23:34:16 +00:00
if script . param . is_empty ( ) {
error! ( " Chat script cryotrip needs a parameter " ) ;
}
else {
2024-04-11 19:30:27 +00:00
if let Ok ( ( mut pos , mut v ) ) = q_playercam . get_single_mut ( ) {
2024-04-10 23:34:16 +00:00
if script . param = = " oscillation " . to_string ( ) {
* pos = Position ( DVec3 ::new ( 147e6 , 165e6 , 336e6 ) ) ;
2024-04-11 19:30:27 +00:00
v . 0 = DVec3 ::ZERO ;
2024-04-10 23:34:16 +00:00
}
2024-04-10 23:55:32 +00:00
else if script . param = = " metisprime " . to_string ( ) {
* pos = Position ( DVec3 ::new ( 27643e3 , - 47e3 , - 124434e3 ) ) ;
2024-04-11 19:30:27 +00:00
v . 0 = DVec3 ::ZERO ;
2024-04-10 23:55:32 +00:00
}
else if script . param = = " serenity " . to_string ( ) {
* pos = Position ( DVec3 ::new ( - 121095e3 , 582e3 , - 190816e3 ) ) ;
2024-04-11 19:30:27 +00:00
v . 0 = DVec3 ::ZERO ;
2024-04-10 23:55:32 +00:00
}
2024-04-10 23:34:16 +00:00
else {
error! ( " Invalid destination for cryotrip chat script: '{}' " , script . param ) ;
}
2024-04-11 00:14:36 +00:00
}
2024-04-11 19:30:27 +00:00
if let Ok ( ( _ , mut suit , mut gforce ) ) = q_player . get_single_mut ( ) {
2024-04-10 23:34:16 +00:00
suit . oxygen = suit . oxygen_max ;
2024-04-11 19:30:27 +00:00
gforce . ignore_gforce_seconds = 1.0 ;
2024-04-10 23:34:16 +00:00
}
ew_sfx . send ( audio ::PlaySfxEvent ( audio ::Sfx ::WakeUp ) ) ;
ew_effect . send ( effects ::SpawnEffectEvent {
class : effects ::Effects ::FadeIn ( Color ::CYAN ) ,
duration : 1.0 ,
} ) ;
}
2024-04-10 23:12:07 +00:00
}
" 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} " ) ;
}
2024-04-04 11:33:54 +00:00
}
}
}
2024-04-04 11:39:49 +00:00
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 ( ) ;
}
}