Merge branch 'james/triggered' into 'master'

Wiring System Documentation and Refactoring

See merge request veloren/veloren!3447
This commit is contained in:
Marcel 2022-06-29 20:28:10 +00:00
commit 5284e9ec94
6 changed files with 353 additions and 527 deletions

View File

@ -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<String>,
_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(

View File

@ -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 {

View File

@ -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<ServerEvent>>,
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<ServerEvent>>,
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<Self>, 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<Self>,
(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<Entity, HashMap<String, f32>> = (
&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::<HashMap<_, _>>(),
)
})
.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<Entity, HashMap<String, f32>>,
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);
}
});
}

View File

@ -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<Entity, HashMap<String, f32>> {
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::<HashMap<_, _>>(),
)
})
.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<String, f32>,
physics_state: Option<&PhysicsState>,
entities_died_last_tick: &Read<EntitiesDiedLastTick>,
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<String, f32>,
physics_state: Option<&PhysicsState>,
entities_died_last_tick: &Read<EntitiesDiedLastTick>,
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<EntitiesDiedLastTick>,
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<String, f32>,
physics_state: Option<&PhysicsState>,
entities_died_last_tick: &Read<EntitiesDiedLastTick>,
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,
}
}

View File

@ -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<String, f32>,
action_effects: &[WiringActionEffect],
server_emitter: &mut Emitter<ServerEvent>,
light_emitter: &mut Option<impl DerefMut<Target = LightEmitter>>,
physics_state: Option<&PhysicsState>,
entities_died_last_tick: &Read<EntitiesDiedLastTick>,
block_change: &mut Write<BlockChange>,
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<ServerEvent>,
) {
// 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<String, f32>,
r: &OutputFormula,
g: &OutputFormula,
b: &OutputFormula,
light_emitter: &mut Option<&mut LightEmitter>,
physics_state: Option<&PhysicsState>,
entities_died_last_tick: &Read<EntitiesDiedLastTick>,
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<i32>,
block: Block,
block_change: &mut Write<BlockChange>,
pos: Option<&Pos>,
) {
let chunk_origin = pos
.map(|opos| {
opos.0
.xy()
.as_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |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);
}

View File

@ -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<String, f32>,
pub outputs: HashMap<String, OutputFormula>,
pub actions: Vec<WiringAction>,
}
/// Connects input to output elements. Required for elements to receive outputs
/// from the proper inputs.
pub struct Circuit {
pub wires: Vec<Wire>,
}
pub enum OutputFormula {
Constant { value: f32 },
Input { name: String },
Logic(Box<Logic>),
SineWave { amplitude: f32, frequency: f32 },
OnCollide { value: f32 },
OnInteract { value: f32 },
OnDeath { value: f32, radius: f32 },
impl Circuit {
pub fn new(wires: Vec<Wire>) -> 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<Logic>),
/// 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<String, f32>,
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<WiringActionEffect>,
}
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<String, f32>,
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_::<i32>()
.map2(TerrainChunkSize::RECT_SIZE.as_::<i32>(), |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<i32>,
block: Block,
},
/// Spawn a projectile.
SpawnProjectile { constr: ProjectileConstructor },
/// Set a terrain block at the provided coordinates.
SetBlock { coords: Vec3<i32>, 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 {