You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

498 lines
16 KiB
Rust

#![allow(dead_code)]
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
#![feature(trim_prefix_suffix)]
mod debug;
mod mono;
use bevy::{picking::hover::HoverMap, platform::hash::RandomState};
use debug::*;
use games::*;
use mono::*;
use std::hash::BuildHasher;
fn main() {
App::new()
.add_plugins(BaseGamePlugin {
name: "trees".into(),
..default()
})
.add_plugins(MonologueAssetsPlugin)
.add_plugins(TreesDebugPlugin)
.add_event::<DialogEvent>()
.add_event::<PlantTree>()
.add_event::<AssignMonologue>()
.init_state::<DialogState>()
.insert_resource(ClearColor(WHITE.into()))
.add_systems(
Startup,
(
init_trees,
init_ui,
load_monologues,
position_camera.after(create_camera_3d),
),
)
// When we're done loading, plant forest
.add_systems(OnEnter(LoadingState::Idle), plant_forest.run_if(run_once))
.add_systems(
Update,
(
// Start a dialog if none are running
start_dialog
.run_if(in_state(DebuggingState::Off))
.run_if(in_state(DialogState::None))
.run_if(on_event::<Pointer<Click>>),
// Close the dialog box if it is idle (not choosing)
end_dialog
.run_if(in_state(DebuggingState::Off))
.run_if(in_state(DialogState::Idle))
.run_if(on_event::<Pointer<Click>>),
handle_plant_tree.run_if(on_event::<PlantTree>),
assign_monologue_to_tree
.run_if(on_event::<AssignMonologue>)
.after(handle_plant_tree),
dialog_engine.run_if(on_event::<DialogEvent>),
auto_scroll.run_if(any_component_added::<DialogOption>),
dialog_box_visibility.run_if(state_changed::<DialogState>),
scale_window.run_if(on_event::<WindowResized>),
),
)
.add_observer(add_tree_monologue)
.add_observer(remove_tree_monologue)
.run();
}
/// Tree marker component
#[derive(Component)]
struct Tree;
#[derive(Component, PartialEq, Clone)]
struct TreeMonologue(Handle<Monologue>);
/// Initialize the trees, currently placeholders
/// Trees are 2d cards in a 3d world for flexibility
/// Might move fully 2d if the art style allows it
fn init_trees(mut ambient_light: ResMut<AmbientLight>) {
// Global light
ambient_light.brightness = 500.0;
}
/// Dialog box marker component
#[derive(Component)]
struct DialogBox;
/// Initialize the UI which consists soley of a dialog box (for now?)
fn init_ui(mut commands: Commands) {
commands
.spawn((
DialogBox,
BackgroundColor(BLACK.with_alpha(0.9).into()),
DialogState::Ongoing,
Node {
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
width: Val::Percent(98.0),
max_height: Val::Percent(50.0),
align_items: AlignItems::Center,
margin: UiRect::all(Val::Percent(1.0)),
padding: UiRect::all(Val::Percent(1.0)),
flex_direction: FlexDirection::Column,
// Scroll on the Y axis
overflow: Overflow::scroll_y(),
..default()
},
))
.observe(scroll)
.observe(hover_dialog_box_over)
.observe(hover_dialog_box_out);
}
fn hover_dialog_box_over(
trigger: Trigger<Pointer<Over>>,
mut query: Query<&mut BackgroundColor, With<DialogBox>>,
) {
if let Ok(mut bg) = query.get_mut(trigger.target()) {
bg.0.set_alpha(0.95);
}
}
fn hover_dialog_box_out(
trigger: Trigger<Pointer<Out>>,
mut query: Query<&mut BackgroundColor, With<DialogBox>>,
) {
if let Ok(mut bg) = query.get_mut(trigger.target()) {
bg.0.set_alpha(0.9);
}
}
/// On startup move the camera to a suitable position
/// This should be mostly static for the entire game
fn position_camera(mut query: Query<&mut Transform, (With<Camera>, With<Camera3d>)>) {
use std::f32::consts::PI;
query.iter_mut().for_each(|mut t| {
*t = Transform::from_xyz(0.0, 100.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y);
t.rotate_y(-PI * 0.5)
})
}
/// Automatically scrolls dialog when a new batch of options are added
fn auto_scroll(
added: Query<Entity, Added<DialogOption>>,
mut scroll_positions: Query<&mut ScrollPosition>,
) {
debug_assert!(
!added.is_empty(),
"Should only scroll when dialog options are added"
);
scroll_positions.iter_mut().for_each(|mut sp| {
sp.offset_y = f32::MAX;
});
}
/// A possible line of dialog the user can choose
#[derive(Component)]
struct DialogOption;
/// A line of dialog which has been chosen, so is permanent
#[derive(Component)]
struct DialogLine;
/// Events that drive the dialog engine
#[derive(Event, PartialEq)]
enum DialogEvent {
Start(Entity, Handle<Monologue>),
NextBatch,
End,
}
/// State tracking if we are actively "doing a monologue"
#[derive(States, Debug, Hash, Eq, PartialEq, Clone, Component, Default)]
enum DialogState {
// Dialog is running and being interacted with
Ongoing,
// There is no more dialog so it's just sitting there
Idle,
// Dialog box is not visible
#[default]
None,
}
/// Start dialog
fn start_dialog(
mut click_events: EventReader<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
query: Query<&TreeMonologue, With<Tree>>,
) {
click_events.read().for_each(|event| {
debug!("Click event detected in start dialog systme");
if let Ok(TreeMonologue(handle)) = query.get(event.target) {
debug!("Tree Monologue received, sending start dialog event");
dialog_events.write(DialogEvent::Start(event.target, handle.clone()));
dialog_events.write(DialogEvent::NextBatch);
}
})
}
/// When dialog is complete and you click away from the dialog box, close it out
fn end_dialog(
mut click_events: EventReader<Pointer<Click>>,
mut dialog_events: EventWriter<DialogEvent>,
query: Query<Entity, Or<(With<TreeMonologue>, With<DialogBox>, With<DialogOption>)>>,
) {
click_events.read().for_each(|event| {
debug!("Click even triggered end of dialog: {:?}", event.target);
if !query.contains(event.target) {
dialog_events.write(DialogEvent::End);
}
});
}
/// System which puts DialogOptions into the DialogBox
fn dialog_engine(
// React to dialog events
mut events: EventReader<DialogEvent>,
// Reference to DialogBox
dialog_box: Single<Entity, With<DialogBox>>,
// EntityCommands for Dialog Box
mut commands: Commands,
// Handle to "active" monologue
mut handle: Local<Handle<Monologue>>,
// Track active entity as well as the monologue
mut tree_entity: Local<Option<Entity>>,
// Index into "active" monologue
mut idx: Local<usize>,
// Inform the rest of the game what state of dialog we are in
mut next_state: ResMut<NextState<DialogState>>,
// Monologue assets for obvious reasons
monologues: Res<Assets<Monologue>>,
// Dialog lines to despawn them at the end/start of a dialog
lines: Query<Entity, With<DialogLine>>,
) {
debug_assert!(
!events.is_empty(),
"Dialog engine is triggered by Dialog Events"
);
events.read().for_each(|event| {
match event {
DialogEvent::Start(e, h) => {
debug!("Dialog start: {:?}", h);
// Set state to "Active"
next_state.set(DialogState::Ongoing);
// Copy monologue asset into local
*handle = h.clone();
*tree_entity = Some(*e);
}
DialogEvent::NextBatch => {
debug!("Dialog batch");
commands.entity(*dialog_box).with_children(|parent| {
// Fetch this monologue from the assets
if let Some(monologue) = monologues.get(handle.clone().id()) {
// Fetch this batch of options
if let Some(batch) = monologue.get(*idx) {
// Spawn the dialog options in the dialog box
batch.lines.iter().for_each(|line| {
parent.spawn((
Text::new(line.clone()),
DialogOption,
TextLayout::new(JustifyText::Left, LineBreak::NoWrap),
));
});
*idx += 1;
} else {
// Set the dialog state to "Idle"
next_state.set(DialogState::Idle);
}
}
});
}
DialogEvent::End => {
debug!("Dialog ended");
// Remove the TreeMonologue component from the tree we just "spoke to"
commands
.entity(tree_entity.unwrap())
.remove::<TreeMonologue>();
// Remove lines from dialog box
lines.iter().for_each(|e| {
commands.entity(e).despawn();
});
// Reset index into the dialog options array
*idx = 0;
// Wipe the current handle from context
*handle = Handle::default();
// Set state to "Active"
next_state.set(DialogState::None);
}
}
});
}
fn hover_dialog_option_over(
trigger: Trigger<Pointer<Over>>,
mut query: Query<(&mut TextColor, &mut BackgroundColor)>,
) {
if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) {
*tc = TextColor(DARK_ORANGE.into());
bg.0.set_alpha(1.0);
}
}
fn hover_dialog_option_out(
trigger: Trigger<Pointer<Out>>,
mut query: Query<(&mut TextColor, &mut BackgroundColor)>,
) {
if let Ok((mut tc, mut bg)) = query.get_mut(trigger.target()) {
*tc = TextColor(ORANGE.into());
bg.0.set_alpha(0.0);
}
}
fn dialog_box_visibility(
state: Res<State<DialogState>>,
mut dialog_box: Single<&mut Visibility, With<DialogBox>>,
) {
**dialog_box = if *state.get() == DialogState::None {
Visibility::Hidden
} else {
Visibility::Inherited
};
}
fn add_tree_monologue(
trigger: Trigger<OnAdd, TreeMonologue>,
query: Query<&MeshMaterial3d<StandardMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Get the affected entity's MeshMaterial3d
if let Ok(handle) = query.get(trigger.target()) {
// Get the concrete StandardMaterial
if let Some(material) = materials.get_mut(handle) {
material.base_color = WHITE.with_alpha(1.0).into();
}
}
}
fn remove_tree_monologue(
trigger: Trigger<OnRemove, TreeMonologue>,
query: Query<&MeshMaterial3d<StandardMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Get the affected entity's MeshMaterial3d
if let Ok(handle) = query.get(trigger.target()) {
// Get the concrete StandardMaterial
if let Some(material) = materials.get_mut(handle) {
// Make it dull
material.base_color = WHITE.with_alpha(0.9).into();
}
}
}
fn scale_window(events: EventReader<WindowResized>, mut window: Single<&mut Window>) {
debug_assert!(!events.is_empty(), "Only scale window when resized");
let r = &mut window.resolution;
let a: f32 = match r.physical_width() as usize {
0..=640 => 0.6,
641..=1280 => 1.0,
1281..=1920 => 1.6,
1921.. => 2.0,
};
let b: f32 = match r.physical_height() as usize {
0..=360 => 0.6,
361..=720 => 1.0,
721..=1080 => 1.6,
1081.. => 2.0,
};
let n = a.min(b);
r.set_scale_factor(n);
debug!(
"Proposed scale factor: ({} -> {a} / {} -> {b}) {n}",
r.width(),
r.height(),
);
}
/// Load all monologues so they are in the asset store and trigger on-load events
fn load_monologues(server: ResMut<AssetServer>, mut loaded_assets: Local<Vec<Handle<Monologue>>>) {
*loaded_assets = include_str!("../../../assets/trees/MONOLOGUES")
.split("\n")
.map(|path| server.load(path))
.collect();
}
#[derive(Event)]
struct PlantTree(Option<Handle<Monologue>>);
/// Plan a tree in the world
/// Handles random placement, 3d model, materials, and observers
fn handle_plant_tree(
mut events: EventReader<PlantTree>,
mut assignments: EventWriter<AssignMonologue>,
trees: Query<Entity, With<Tree>>,
server: Res<AssetServer>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
events.read().for_each(|PlantTree(assignment)| {
debug!("Planing tree");
let mut tree = commands.spawn(Tree);
// Generate "random" X and Y Coordinates for this tree
// 1. Take the top 2 bytes
// 2. Interpet as u8s
// 3. Re-interpret as i8s
// 4. Cast to f32
let transform = {
let n = RandomState::default().hash_one(tree.id());
let [a, b, ..] = n.to_be_bytes();
let x: f32 = a.cast_signed().wrapping_div(4).into();
let y: f32 = b.cast_signed().wrapping_div(4).into();
// Avoid mesh clipping by offsetting each on the z axis
let z = trees.iter().len() as f32;
debug!("Coordiantes: {x} {y}");
Transform::from_xyz(x, z, y).with_scale(Vec3::splat(10.0))
};
let material = MeshMaterial3d(materials.add(StandardMaterial {
base_color_texture: Some(server.load("trees/placeholder/tree.png")),
base_color: WHITE.with_alpha(0.9).into(),
alpha_mode: AlphaMode::Blend,
..default()
}));
let mesh = Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(1.0))));
tree.insert((mesh, material, transform));
if let Some(handle) = assignment {
assignments.write(AssignMonologue(handle.clone()));
}
});
}
#[derive(Event, Debug)]
struct AssignMonologue(Handle<Monologue>);
/// Assign the given monologue to a tree
fn assign_monologue_to_tree(
mut events: EventReader<AssignMonologue>,
query: Query<Entity, (With<Tree>, Without<TreeMonologue>)>,
mut notice: ResMut<Notice>,
mut commands: Commands,
) {
// Kinda a weird hack because query does not update
// If we do this inline we assign new monologues to the same first tree
let mut t = query.iter();
events.read().for_each(|event| {
debug!("Assigning monologue {:?}", event);
// Get a valid tree to assign an entity to
if let Some(tree) = t.next() {
// Create the TreeMonologue component
let monologue = TreeMonologue(event.0.clone());
// Insert the component to the entity
commands.entity(tree).insert(monologue);
} else if let Some(path) = event.0.path() {
error!("No trees avaliable for {path:?}");
notice.0 = format!("No trees avaliable for {path:?}");
} else {
error!("Monologue is not yet loaded!");
notice.0 = "Monologue is not yet loaded!".into();
}
});
}
/// On startup, plant a forest (add a few trees to the game)
fn plant_forest(monos: Res<Assets<Monologue>>, mut e_trees: EventWriter<PlantTree>) {
let mut i = 10;
for id in monos.ids() {
debug!("Planting tree with monologue {:?}", id);
if i > 5 {
e_trees.write(PlantTree(Some(Handle::Weak(id))));
} else if i > 0 {
e_trees.write(PlantTree(None));
} else {
break;
}
i -= 1;
}
}