diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 4b5edf8778..1e89a9923f 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -11,7 +11,7 @@ use crate::{ }, sys::terrain::NpcData, wiring, - wiring::{Logic, OutputFormula}, + wiring::OutputFormula, Server, Settings, SpawnPoint, StateExt, }; use assets::AssetExt; @@ -60,7 +60,7 @@ use specs::{ }; use std::{str::FromStr, sync::Arc}; use vek::*; -use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement}; +use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement}; use world::util::Sampler; use common::comp::Alignment; @@ -2200,118 +2200,58 @@ fn handle_spawn_wiring( _args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - // Obviously it is a WIP - use it for debug - let mut pos = position(server, target, "target")?; pos.0.x += 3.0; let mut outputs1 = HashMap::new(); - outputs1.insert( - "deaths_last_tick".to_string(), - wiring::OutputFormula::OnDeath { - value: 1.0, - radius: 30.0, - }, - ); - outputs1.insert( - "deaths_accumulated".to_string(), - OutputFormula::Logic(Box::new(Logic { - kind: wiring::LogicKind::Sum, - left: OutputFormula::Logic(Box::new(Logic { - kind: wiring::LogicKind::Sub, - left: OutputFormula::Input { - name: "deaths_accumulated".to_string(), - }, - right: OutputFormula::Logic(Box::new(Logic { - kind: wiring::LogicKind::Min, - left: OutputFormula::Input { - name: "pressed".to_string(), - }, - right: OutputFormula::Input { - name: "deaths_accumulated".to_string(), - }, - })), - })), - right: OutputFormula::Input { - name: "deaths_last_tick".to_string(), - }, - })), - ); - outputs1.insert("pressed".to_string(), OutputFormula::OnCollide { - value: f32::MAX, + outputs1.insert("button".to_string(), OutputFormula::OnCollide { + value: 1.0, }); + // Create the first element of the circuit. + // This is a coin body. This element does not have any inputs or actions. + // Instead there is one output. When there is a collision with this element the + // value of 1.0 will be sent as an input with the "button" label. Any + // element with an `Input` for the name "button" can use this value as an + // input. The threshold does not matter as there are no effects for this + // element. let builder1 = server .state .create_wiring(pos, comp::object::Body::Coins, WiringElement { - actions: vec![WiringAction { - formula: wiring::OutputFormula::Constant { value: 1.0 }, - threshold: 1.0, - effects: vec![WiringActionEffect::SetLight { - r: wiring::OutputFormula::Input { - name: String::from("color"), - }, - g: wiring::OutputFormula::Input { - name: String::from("color"), - }, - b: wiring::OutputFormula::Input { - name: String::from("color"), - }, - }], - }], inputs: HashMap::new(), outputs: outputs1, + actions: Vec::new(), }) - .with(comp::Density(100_f32)) - .with(comp::Sticky); + .with(comp::Density(100_f32)); let ent1 = builder1.build(); pos.0.x += 3.0; + // The second element has no elements in the `inputs` field to start with. When + // the circuit runs, the input as specified by the `Input` OutputFormula is + // added to the inputs. The next tick the effect(s) are applied based on the + // input value. let builder2 = server .state .create_wiring(pos, comp::object::Body::Coins, WiringElement { - actions: vec![ - WiringAction { - formula: wiring::OutputFormula::Input { - name: String::from("deaths_accumulated"), - }, - threshold: 5.0, - effects: vec![WiringActionEffect::SpawnProjectile { - constr: comp::ProjectileConstructor::Arrow { - damage: 1.0, - energy_regen: 0.0, - knockback: 0.0, - }, - }], - }, - WiringAction { - formula: wiring::OutputFormula::Input { - name: String::from("deaths_accumulated"), - }, - threshold: 1.0, - effects: vec![WiringActionEffect::SetBlock { - coords: vek::Vec3::new(0, 0, pos.0.z as i32), - block: Block::new(BlockKind::Water, vek::Rgb::new(0, 0, 0)), - }], - }, - WiringAction { - formula: wiring::OutputFormula::Constant { value: 1.0 }, - threshold: 1.0, - effects: vec![WiringActionEffect::SetLight { - r: wiring::OutputFormula::Input { - name: String::from("color"), - }, - g: wiring::OutputFormula::Input { - name: String::from("color"), - }, - b: wiring::OutputFormula::Input { - name: String::from("color"), - }, - }], - }, - ], inputs: HashMap::new(), outputs: HashMap::new(), + actions: vec![WiringAction { + formula: OutputFormula::Input { + name: String::from("button"), + }, + threshold: 0.0, + effects: vec![WiringActionEffect::SetLight { + r: OutputFormula::Input { + name: String::from("button"), + }, + g: OutputFormula::Input { + name: String::from("button"), + }, + b: OutputFormula::Input { + name: String::from("button"), + }, + }], + }], }) .with(comp::Density(100_f32)); let ent2 = builder2.build(); @@ -2320,39 +2260,15 @@ fn handle_spawn_wiring( let builder3 = server .state .create_wiring(pos, comp::object::Body::TrainingDummy, WiringElement { - actions: vec![], inputs: HashMap::new(), outputs: HashMap::new(), + actions: Vec::new(), }) .with(comp::Density(comp::object::Body::TrainingDummy.density().0)) - .with(Circuit { - wires: vec![ - Wire { - input_entity: ent1, - input_field: String::from("deaths_last_tick"), - output_entity: ent1, - output_field: String::from("deaths_last_tick"), - }, - Wire { - input_entity: ent1, - input_field: String::from("deaths_accumulated"), - output_entity: ent1, - output_field: String::from("deaths_accumulated"), - }, - Wire { - input_entity: ent1, - input_field: String::from("pressed"), - output_entity: ent1, - output_field: String::from("pressed"), - }, - Wire { - input_entity: ent1, - input_field: String::from("deaths_accumulated"), - output_entity: ent2, - output_field: String::from("deaths_accumulated"), - }, - ], - }); + .with(Circuit::new(vec![Wire { + input: WireNode::new(ent1, "button".to_string()), + output: WireNode::new(ent2, "button".to_string()), + }])); builder3.build(); server.notify_client( diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index d3057bd6b4..53b1bcceab 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -422,7 +422,7 @@ impl StateExt for State { .with(comp::Ori::default()) .with(capsule(&object.into())) .with(comp::Body::Object(object)) - .with(comp::Mass(10.0)) + .with(comp::Mass(100.0)) // .with(comp::Sticky) .with(wiring_element) .with(comp::LightEmitter { diff --git a/server/src/sys/wiring.rs b/server/src/sys/wiring.rs index 7eff67305a..3c563eedd0 100644 --- a/server/src/sys/wiring.rs +++ b/server/src/sys/wiring.rs @@ -11,72 +11,131 @@ use specs::{ join::Join, shred::ResourceId, Entities, Entity, Read, ReadStorage, SystemData, World, Write, WriteStorage, }; -mod compute_outputs; -use compute_outputs::compute_outputs; -mod dispatch_actions; -use dispatch_actions::dispatch_actions; #[derive(SystemData)] -pub struct WiringData<'a> { - pub circuits: ReadStorage<'a, Circuit>, - pub wiring_elements: WriteStorage<'a, WiringElement>, - - pub entities: Entities<'a>, - - pub light_emitters: WriteStorage<'a, LightEmitter>, // maybe - pub physics_states: ReadStorage<'a, PhysicsState>, // maybe - pub pos: ReadStorage<'a, Pos>, - - pub event_bus: Read<'a, EventBus>, - pub entities_died_last_tick: Read<'a, EntitiesDiedLastTick>, - pub block_change: Write<'a, BlockChange>, +pub struct ReadData<'a> { + entities: Entities<'a>, + circuits: ReadStorage<'a, Circuit>, + pos: ReadStorage<'a, Pos>, + physics_states: ReadStorage<'a, PhysicsState>, + entities_died_last_tick: Read<'a, EntitiesDiedLastTick>, } /// This system is responsible for handling wiring (signals and wiring systems) #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { - type SystemData = WiringData<'a>; + type SystemData = ( + ReadData<'a>, + Read<'a, EventBus>, + WriteStorage<'a, WiringElement>, + WriteStorage<'a, LightEmitter>, // maybe + Write<'a, BlockChange>, + ); const NAME: &'static str = "wiring"; const ORIGIN: Origin = Origin::Server; const PHASE: Phase = Phase::Create; - fn run(_job: &mut Job, mut system_data: Self::SystemData) { - // Calculate new outputs using inputs (those inputs are calculated and populated - // in previous tick) Take inputs and wiring_element.outputs and with - // that compute new outputs - let computed_outputs = compute_outputs(&system_data); - // Pass new outputs as inputs for the next tick - dispatch_circuit_transport(&computed_outputs, &mut system_data); - // Using inputs dispatch actions - dispatch_actions(&mut system_data); + fn run( + _job: &mut Job, + (read_data, event_bus, mut wiring_elements, mut light_emitters, mut block_change): Self::SystemData, + ) { + let mut server_emitter = event_bus.emitter(); + + // Compute the output for each wiring element by computing + // the output for each `OutputFormula` and store each value per output per + // entity. + let computed_outputs: HashMap> = ( + &read_data.entities, + &wiring_elements, + read_data.physics_states.maybe(), + read_data.pos.maybe(), + ) + .join() + .map(|(entity, wiring_element, physics_state, pos)| { + ( + entity, + wiring_element + .outputs + .iter() + .map(|(key, output_formula)| { + ( + // Output name/key + key.to_string(), + // Output value + output_formula.compute_output( + &wiring_element.inputs, + physics_state, + &read_data.entities_died_last_tick.0, + pos, + ), + ) + }) + .collect::>(), + ) + }) + .collect(); + + // Pass new outputs as inputs for the next tick to the proper elements in the + // circuit. + (read_data.circuits) + .join() + .flat_map(|circuit| circuit.wires.iter()) + .for_each(|wire| { + // The current output values becomes input values for the next tick + let input_value = computed_outputs + .get(&wire.input.entity) + .and_then(|e| e.get(&wire.input.name)) + .unwrap_or(&0.0); + + // Push the current output value into the inputs for the proper element to be + // used next tick. + if let Some(wiring_element) = wiring_elements.get_mut(wire.output.entity) { + wiring_element + .inputs + .insert(wire.output.name.clone(), *input_value); + } + }); + + // Use inputs to dispatch actions and apply effects + ( + &read_data.entities, + &mut wiring_elements, + read_data.physics_states.maybe(), + (&mut light_emitters).maybe(), + read_data.pos.maybe(), + ) + .join() + .for_each( + |(entity, wiring_element, physics_state, mut light_emitter, pos)| { + wiring_element + .actions + .iter() + .filter(|wiring_action| { + // Filter out any wiring actions with a total output less than the + // threshold + wiring_action.formula.compute_output( + &wiring_element.inputs, + physics_state, + &read_data.entities_died_last_tick.0, + pos, + ) >= wiring_action.threshold + }) + .for_each(|wiring_action| { + // Apply world effects of each wiring action + wiring_action.apply_effects( + entity, + &wiring_element.inputs, + physics_state, + &read_data.entities_died_last_tick.0, + &mut server_emitter, + pos, + &mut block_change, + light_emitter.as_deref_mut(), + ); + }) + }, + ) } } - -fn dispatch_circuit_transport<'a>( - computed_outputs: &HashMap>, - system_data: &mut WiringData<'a>, -) { - let WiringData { - circuits, - wiring_elements, - .. - } = system_data; - - (circuits) - .join() - .flat_map(|circuit| circuit.wires.iter()) - .for_each(|wire| { - let input_value = computed_outputs - .get(&wire.input_entity) - .and_then(|e| e.get(&wire.input_field)) - .unwrap_or(&0.0); - - if let Some(wiring_element) = wiring_elements.get_mut(wire.output_entity) { - wiring_element - .inputs - .insert(wire.output_field.clone(), *input_value); - } - }); -} diff --git a/server/src/sys/wiring/compute_outputs.rs b/server/src/sys/wiring/compute_outputs.rs deleted file mode 100644 index ef67da71f8..0000000000 --- a/server/src/sys/wiring/compute_outputs.rs +++ /dev/null @@ -1,159 +0,0 @@ -use super::WiringData; -use crate::wiring::{Logic, LogicKind, OutputFormula}; -use common::{ - comp::{PhysicsState, Pos}, - resources::EntitiesDiedLastTick, -}; -use hashbrown::HashMap; -use rand_distr::num_traits::ToPrimitive; -use specs::{join::Join, Entity, Read}; -use tracing::warn; - -pub fn compute_outputs(system_data: &WiringData) -> HashMap> { - let WiringData { - entities, - wiring_elements, - physics_states, - entities_died_last_tick, - pos, - .. - } = system_data; - ( - &*entities, - wiring_elements, - physics_states.maybe(), - pos.maybe(), - ) - .join() - .map(|(entity, wiring_element, physics_state, pos)| { - ( - entity, - wiring_element - .outputs - .iter() - .map( - |(key, output_formula)| { - compute_output_with_key( - key, - output_formula, - &wiring_element.inputs, - physics_state, - entities_died_last_tick, - pos, - ) - }, // (String, f32) - ) - .collect::>(), - ) - }) - .collect() -} - -pub fn compute_output_with_key( - // yes, this function is defined only to make one place - // look a bit nicer - // Don't discuss. - key: &str, - output_formula: &OutputFormula, - inputs: &HashMap, - physics_state: Option<&PhysicsState>, - entities_died_last_tick: &Read, - pos: Option<&Pos>, -) -> (String, f32) { - ( - key.to_string(), - compute_output( - output_formula, - inputs, - physics_state, - entities_died_last_tick, - pos, - ), - ) -} - -pub fn compute_output( - output_formula: &OutputFormula, - inputs: &HashMap, - physics_state: Option<&PhysicsState>, - entities_died_last_tick: &Read, - pos: Option<&Pos>, -) -> f32 { - match output_formula { - OutputFormula::Constant { value } => *value, - OutputFormula::Input { name } => *inputs.get(name).unwrap_or(&0.0), - OutputFormula::Logic(logic) => { - output_formula_logic(logic, inputs, physics_state, entities_died_last_tick, pos) - }, - OutputFormula::SineWave { .. } => { - warn!("Not implemented OutputFormula::SineWave"); - 0.0 - }, - OutputFormula::OnCollide { value } => output_formula_on_collide(value, physics_state), - OutputFormula::OnInteract { .. } => { - warn!("Not implemented OutputFormula::OnInteract"); - 0.0 - }, - OutputFormula::OnDeath { value, radius } => { - output_formula_on_death(value, radius, entities_died_last_tick, pos) - }, - } -} - -fn output_formula_on_collide(value: &f32, physics_state: Option<&PhysicsState>) -> f32 { - if let Some(ps) = physics_state { - if !ps.touch_entities.is_empty() { - return *value; - } - } - 0.0 -} - -fn output_formula_on_death( - value: &f32, - radius: &f32, - entities_died_last_tick: &Read, - pos: Option<&Pos>, -) -> f32 { - if let Some(pos_of_entity) = pos { - return *value - * entities_died_last_tick - .0 - .iter() - .filter(|(_, dead_pos)| pos_of_entity.0.distance(dead_pos.0) <= *radius) - .count() - .to_f32() - .unwrap_or(0.0); - } - 0.0 -} - -fn output_formula_logic( - logic: &Logic, - inputs: &HashMap, - physics_state: Option<&PhysicsState>, - entities_died_last_tick: &Read, - pos: Option<&Pos>, -) -> f32 { - let left = compute_output( - &logic.left, - inputs, - physics_state, - entities_died_last_tick, - pos, - ); - let right = compute_output( - &logic.right, - inputs, - physics_state, - entities_died_last_tick, - pos, - ); - match logic.kind { - LogicKind::Max => f32::max(left, right), - LogicKind::Min => f32::min(left, right), - LogicKind::Sub => left - right, - LogicKind::Sum => left + right, - LogicKind::Mul => left * right, - } -} diff --git a/server/src/sys/wiring/dispatch_actions.rs b/server/src/sys/wiring/dispatch_actions.rs deleted file mode 100644 index b6a88fde3c..0000000000 --- a/server/src/sys/wiring/dispatch_actions.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::ops::DerefMut; - -use super::{compute_outputs::compute_output, WiringData}; -use crate::wiring::{OutputFormula, WiringActionEffect}; -use common::{ - comp::{object, Body, LightEmitter, PhysicsState, Pos, ProjectileConstructor}, - event::{Emitter, ServerEvent}, - resources::EntitiesDiedLastTick, - terrain::{block::Block, TerrainChunkSize}, - util::Dir, - vol::RectVolSize, -}; -use common_state::BlockChange; -use hashbrown::HashMap; -use specs::{join::Join, Entity, Read, Write}; -use vek::{Rgb, Vec3}; - -pub fn dispatch_actions(system_data: &mut WiringData) { - let WiringData { - entities, - event_bus, - wiring_elements, - physics_states, - light_emitters, - entities_died_last_tick, - block_change, - pos, - .. - } = system_data; - let mut server_emitter = event_bus.emitter(); - - ( - &*entities, - wiring_elements, - physics_states.maybe(), - light_emitters.maybe(), - pos.maybe(), - ) - .join() - .for_each( - |(entity, wiring_element, physics_state, mut light_emitter, pos)| { - wiring_element - .actions - .iter() - .filter(|wiring_action| { - compute_output( - &wiring_action.formula, - &wiring_element.inputs, - physics_state, - entities_died_last_tick, - pos, - ) >= wiring_action.threshold - }) - .for_each(|wiring_action| { - dispatch_action( - entity, - &wiring_element.inputs, - &wiring_action.effects, - &mut server_emitter, - &mut light_emitter, - physics_state, - entities_died_last_tick, - block_change, - pos, - ); - }) - }, - ) -} - -fn dispatch_action( - entity: Entity, - inputs: &HashMap, - action_effects: &[WiringActionEffect], - - server_emitter: &mut Emitter, - - light_emitter: &mut Option>, - physics_state: Option<&PhysicsState>, - entities_died_last_tick: &Read, - block_change: &mut Write, - pos: Option<&Pos>, -) { - action_effects - .iter() - .for_each(|action_effect| match action_effect { - WiringActionEffect::SetBlock { coords, block } => { - dispatch_action_set_block(*coords, *block, block_change, pos); - }, - WiringActionEffect::SpawnProjectile { constr } => { - dispatch_action_spawn_projectile(entity, constr, server_emitter) - }, - WiringActionEffect::SetLight { r, g, b } => dispatch_action_set_light( - inputs, - r, - g, - b, - &mut light_emitter.as_deref_mut(), - physics_state, - entities_died_last_tick, - pos, - ), - }); -} - -fn dispatch_action_spawn_projectile( - entity: Entity, - constr: &ProjectileConstructor, - server_emitter: &mut Emitter, -) { - // Use match here if there will be more options - // NOTE: constr in RFC is about Arrow projectile - server_emitter.emit(ServerEvent::Shoot { - entity, - pos: Pos(Vec3::zero()), - dir: Dir::forward(), - body: Body::Object(object::Body::Arrow), - projectile: constr.create_projectile(None, 0.0, 1.0, 1.0), - light: None, - speed: 5.0, - object: None, - }); -} - -fn dispatch_action_set_light( - inputs: &HashMap, - r: &OutputFormula, - g: &OutputFormula, - b: &OutputFormula, - - light_emitter: &mut Option<&mut LightEmitter>, - - physics_state: Option<&PhysicsState>, - entities_died_last_tick: &Read, - pos: Option<&Pos>, -) { - if let Some(light_emitter) = light_emitter { - // TODO: make compute_output accept multiple formulas - - let computed_r = compute_output(r, inputs, physics_state, entities_died_last_tick, pos); - let computed_g = compute_output(g, inputs, physics_state, entities_died_last_tick, pos); - let computed_b = compute_output(b, inputs, physics_state, entities_died_last_tick, pos); - - light_emitter.col = Rgb::new(computed_r, computed_g, computed_b); - } -} - -fn dispatch_action_set_block( - coord: vek::Vec3, - block: Block, - block_change: &mut Write, - pos: Option<&Pos>, -) { - let chunk_origin = pos - .map(|opos| { - opos.0 - .xy() - .as_::() - .map2(TerrainChunkSize::RECT_SIZE.as_::(), |a, b| (a / b) * b) - .with_z(0) - }) - .unwrap_or_else(vek::Vec3::zero); - let offset_pos = chunk_origin + coord; - block_change.set(offset_pos, block); -} diff --git a/server/src/wiring.rs b/server/src/wiring.rs index 9759a83e30..b8999d371f 100644 --- a/server/src/wiring.rs +++ b/server/src/wiring.rs @@ -1,58 +1,224 @@ -use common::{comp::ProjectileConstructor, terrain::Block}; +use common::{ + comp::{object, Body, LightEmitter, PhysicsState, Pos, ProjectileConstructor}, + event::{Emitter, ServerEvent}, + terrain::{Block, TerrainChunkSize}, + util::Dir, + vol::RectVolSize, +}; +use common_state::BlockChange; use hashbrown::HashMap; use specs::{Component, Entity}; use specs_idvs::IdvStorage; -use vek::Vec3; +use tracing::warn; +use vek::{num_traits::ToPrimitive, Rgb, Vec3}; +/// Represents a logical operation based on a `left` and `right` input. The +/// available kinds of logical operations are enumerated by `LogicKind`. pub struct Logic { pub kind: LogicKind, pub left: OutputFormula, pub right: OutputFormula, } +/// The basic element of the wiring system. Inputs are dynamically added based +/// on the outputs of other elements. Actions specify what to output or what +/// inputs to read as well as what effects the values should have on the world +/// state (eg. emit a projectile). pub struct WiringElement { pub inputs: HashMap, pub outputs: HashMap, pub actions: Vec, } +/// Connects input to output elements. Required for elements to receive outputs +/// from the proper inputs. pub struct Circuit { pub wires: Vec, } -pub enum OutputFormula { - Constant { value: f32 }, - Input { name: String }, - Logic(Box), - - SineWave { amplitude: f32, frequency: f32 }, - OnCollide { value: f32 }, - OnInteract { value: f32 }, - OnDeath { value: f32, radius: f32 }, +impl Circuit { + pub fn new(wires: Vec) -> Self { Self { wires } } } +/// Represents an output for a `WiringAction`. The total output can be constant, +/// directly from an input, based on collision state, or based on custom logic. +pub enum OutputFormula { + /// Returns a constant value + Constant { value: f32 }, + /// Retrieves the value from a string identified input. A wiring element can + /// have multiple inputs. + Input { name: String }, + /// Performs a logic operation on the `left` and `right` values of the + /// provided `Logic`. The operation is specified by the `LogicKind`. + /// Operations include `Min`, `Max`, `Sub`, `Sum`, and `Mul`. + Logic(Box), + /// Returns `value` if the wiring element is in contact with another entity + /// with collision. + OnCollide { value: f32 }, + /// Returns `value` if an entity died in the last tick within `radius` of + /// the wiring element. + OnDeath { value: f32, radius: f32 }, + + // TODO: The following `OutputFormula`s are unimplemented!!!! + /// Returns an oscillating value based on the sine wave with `amplitude` and + /// `frequency`. + SineWave { amplitude: f32, frequency: f32 }, + /// Returns `value` when the wiring element in interacted with. + OnInteract { value: f32 }, +} + +impl OutputFormula { + /// Computes the output of an `OutputFormula` as an `f32` based on the + /// inputs and world state. Currently that world state only includes + /// physics state, position, and the list of entities that died in the + /// last tick. + pub fn compute_output( + &self, + inputs: &HashMap, + physics_state: Option<&PhysicsState>, + entities_died_last_tick: &Vec<(Entity, Pos)>, + pos: Option<&Pos>, + ) -> f32 { + match self { + OutputFormula::Constant { value } => *value, + OutputFormula::Input { name } => *inputs.get(name).unwrap_or(&0.0), + OutputFormula::Logic(logic) => { + let left = + &logic + .left + .compute_output(inputs, physics_state, entities_died_last_tick, pos); + let right = &logic.right.compute_output( + inputs, + physics_state, + entities_died_last_tick, + pos, + ); + match logic.kind { + LogicKind::Max => f32::max(*left, *right), + LogicKind::Min => f32::min(*left, *right), + LogicKind::Sub => left - right, + LogicKind::Sum => left + right, + LogicKind::Mul => left * right, + } + }, + OutputFormula::OnCollide { value } => physics_state.map_or(0.0, |ps| { + if ps.touch_entities.is_empty() { + 0.0 + } else { + *value + } + }), + OutputFormula::SineWave { .. } => { + warn!("Not implemented OutputFormula::SineWave"); + 0.0 + }, + OutputFormula::OnInteract { .. } => { + warn!("Not implemented OutputFormula::OnInteract"); + 0.0 + }, + OutputFormula::OnDeath { value, radius } => pos.map_or(0.0, |e_pos| { + *value + * entities_died_last_tick + .iter() + .filter(|(_, dead_pos)| e_pos.0.distance(dead_pos.0) <= *radius) + .count() + .to_f32() + .unwrap_or(0.0) + }), + } + } +} + +/// Logical operations applied to two floats. pub enum LogicKind { - Min, // acts like And - Max, // acts like Or - Sub, // `|x| { 5.0 - x }` acts like Not, depending on reference voltages + /// Returns the minimum of `left` and `right`. Acts like And. + Min, + /// Returns the maximum of `left` and `right`. Acts like Or. + Max, + /// Returns `left` minus `right`. Acts like Not, depending on referance + /// values. + Sub, + /// Returns `left` plus `right`. Sum, + /// Returns `left` times `right`. Mul, } +/// Determines what kind of output an element produces (or input is read) based +/// on the `formula`. The `threshold` is the minimum computed output for effects +/// to take place. Effects refer to effects in the game world such as emitting +/// light. pub struct WiringAction { pub formula: OutputFormula, pub threshold: f32, pub effects: Vec, } +impl WiringAction { + /// Applies all effects on the world (such as turning on a light etc.) if + /// the output of the `formula` is greater than `threshold`. + pub fn apply_effects( + &self, + entity: Entity, + inputs: &HashMap, + physics_state: Option<&PhysicsState>, + entities_died_last_tick: &Vec<(Entity, Pos)>, + server_emitter: &mut Emitter<'_, ServerEvent>, + pos: Option<&Pos>, + block_change: &mut BlockChange, + mut light_emitter: Option<&mut LightEmitter>, + ) { + self.effects + .iter() + .for_each(|action_effect| match action_effect { + WiringActionEffect::SetBlock { coords, block } => { + let chunk_origin = pos.map_or(Vec3::zero(), |opos| { + opos.0 + .xy() + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |a, b| (a / b) * b) + .with_z(0) + }); + let offset_pos = chunk_origin + coords; + block_change.set(offset_pos, *block); + }, + WiringActionEffect::SpawnProjectile { constr } => { + if let Some(&pos) = pos { + server_emitter.emit(ServerEvent::Shoot { + entity, + pos, + dir: Dir::forward(), + body: Body::Object(object::Body::Arrow), + projectile: constr.create_projectile(None, 0.0, 1.0, 1.0), + light: None, + speed: 5.0, + object: None, + }); + } + }, + WiringActionEffect::SetLight { r, g, b } => { + if let Some(light_emitter) = &mut light_emitter { + let computed_r = + r.compute_output(inputs, physics_state, entities_died_last_tick, pos); + let computed_g = + g.compute_output(inputs, physics_state, entities_died_last_tick, pos); + let computed_b = + b.compute_output(inputs, physics_state, entities_died_last_tick, pos); + + light_emitter.col = Rgb::new(computed_r, computed_g, computed_b); + } + }, + }); + } +} + +/// Effects of a circuit in the game world. pub enum WiringActionEffect { - SpawnProjectile { - constr: ProjectileConstructor, - }, - SetBlock { - coords: Vec3, - block: Block, - }, + /// Spawn a projectile. + SpawnProjectile { constr: ProjectileConstructor }, + /// Set a terrain block at the provided coordinates. + SetBlock { coords: Vec3, block: Block }, + /// Emit light with the given RGB values. SetLight { r: OutputFormula, g: OutputFormula, @@ -60,11 +226,20 @@ pub enum WiringActionEffect { }, } +/// Holds an input and output node. pub struct Wire { - pub input_field: String, - pub output_field: String, - pub input_entity: Entity, - pub output_entity: Entity, + pub input: WireNode, + pub output: WireNode, +} + +/// Represents a node in the circuit. Each node is an entity with a name. +pub struct WireNode { + pub entity: Entity, + pub name: String, +} + +impl WireNode { + pub fn new(entity: Entity, name: String) -> Self { Self { entity, name } } } impl Component for WiringElement {