This commit is contained in:
TotallyNot 2025-11-03 16:36:45 +01:00
parent e7d6b74aab
commit 35413b563c
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
33 changed files with 10238 additions and 1891 deletions

200
models/src/bundle/armour.rs Normal file
View file

@ -0,0 +1,200 @@
use bevy_ecs::{component::Component, entity::Entity};
use rand::distr::Distribution;
use strum::Display;
use crate::{bundle::player::BodyPart, dto::armour::CoverageDto};
#[derive(Clone, Copy, Debug, Display, Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum Immunity {
Radiation,
NerveGas,
TearGas,
PepperSpray,
FlashGrenade,
ConcussionGrenade,
Insanity,
}
#[derive(Component, Default)]
pub struct Immunities(pub Vec<Immunity>);
#[derive(Component, Default)]
pub struct Armour;
#[derive(Component, Default)]
pub struct ArmourCoverage(pub ArmourVec<f32>);
impl From<CoverageDto> for ArmourCoverage {
fn from(value: CoverageDto) -> Self {
let mut result = Self::default();
result.0[ArmourBodyPartSlot::Heart] = value.heart;
result.0[ArmourBodyPartSlot::Stomach] = value.stomach;
result.0[ArmourBodyPartSlot::Chest] = value.chest;
result.0[ArmourBodyPartSlot::Arms] = value.arm;
result.0[ArmourBodyPartSlot::Groin] = value.groin;
result.0[ArmourBodyPartSlot::Legs] = value.leg;
result.0[ArmourBodyPartSlot::Throat] = value.throat;
result.0[ArmourBodyPartSlot::Hands] = value.hand;
result.0[ArmourBodyPartSlot::Feet] = value.foot;
result.0[ArmourBodyPartSlot::Head] = value.head;
result
}
}
#[derive(Component, Default)]
pub struct ArmourValue(pub f32);
enum ArmourIterState {
Head,
Body,
Legs,
Feet,
Hands,
}
#[derive(Debug, Clone, Component)]
pub struct PlayerArmour {
pub head: Option<Entity>,
pub torso: Option<Entity>,
pub legs: Option<Entity>,
pub feet: Option<Entity>,
pub hands: Option<Entity>,
}
#[derive(Debug)]
pub struct BodyPartCoverage {
pub armour: Entity,
pub coverage: f32,
pub armour_value: f32,
}
#[derive(Component, Default, Debug)]
pub struct ArmourBodyPart {
pub armour_pieces: Vec<BodyPartCoverage>,
}
#[derive(Component, Debug)]
pub struct ArmourBodyParts(pub ArmourVec<Entity>);
pub struct ArmourIter<'a> {
state: Option<ArmourIterState>,
equipped_armour: &'a PlayerArmour,
}
impl<'a> Iterator for ArmourIter<'a> {
type Item = Entity;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (next, piece) = match self.state {
None => (ArmourIterState::Head, self.equipped_armour.head),
Some(ArmourIterState::Head) => (ArmourIterState::Body, self.equipped_armour.torso),
Some(ArmourIterState::Body) => (ArmourIterState::Legs, self.equipped_armour.legs),
Some(ArmourIterState::Legs) => (ArmourIterState::Feet, self.equipped_armour.feet),
Some(ArmourIterState::Feet) => (ArmourIterState::Hands, self.equipped_armour.hands),
Some(ArmourIterState::Hands) => return None,
};
self.state = Some(next);
if piece.is_some() {
return piece;
}
}
}
}
impl<'a> IntoIterator for &'a PlayerArmour {
type Item = Entity;
type IntoIter = ArmourIter<'a>;
fn into_iter(self) -> Self::IntoIter {
ArmourIter {
state: None,
equipped_armour: self,
}
}
}
#[repr(usize)]
#[derive(Clone, Copy, strum::EnumIter)]
pub enum ArmourBodyPartSlot {
Arms,
Stomach,
Heart,
Chest,
Throat,
Hands,
Groin,
Legs,
Head,
Feet,
}
impl From<BodyPart> for ArmourBodyPartSlot {
fn from(value: BodyPart) -> Self {
match value {
BodyPart::LeftArm | BodyPart::RightArm => Self::Arms,
BodyPart::Stomach => Self::Stomach,
BodyPart::Heart => Self::Heart,
BodyPart::Chest => Self::Chest,
BodyPart::Throat => Self::Throat,
BodyPart::LeftHand | BodyPart::RightHand => Self::Hands,
BodyPart::Groin => Self::Groin,
BodyPart::LeftLeg | BodyPart::RightLeg => Self::Legs,
BodyPart::Head => Self::Head,
BodyPart::LeftFoot | BodyPart::RightFoot => Self::Feet,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ArmourVec<T>(pub [T; 10]);
impl<T> std::ops::Index<ArmourBodyPartSlot> for ArmourVec<T> {
type Output = T;
fn index(&self, index: ArmourBodyPartSlot) -> &Self::Output {
&self.0[index as usize]
}
}
impl<T> std::ops::IndexMut<ArmourBodyPartSlot> for ArmourVec<T> {
fn index_mut(&mut self, index: ArmourBodyPartSlot) -> &mut Self::Output {
&mut self.0[index as usize]
}
}
impl<T> IntoIterator for ArmourVec<T> {
type Item = T;
type IntoIter = std::array::IntoIter<T, 10>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> Distribution<Option<&'a BodyPartCoverage>> for &'a ArmourBodyPart {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a BodyPartCoverage> {
let mut current = None;
for piece in &self.armour_pieces {
// NOTE: This is not strictly speaking correct, but the edge cases where this applies
// should be very rare, and it should be a decent enough heuristic to barely make a
// difference from the actual pixel comparisons that torn seems to be using for this.
if current
.map(|c: &BodyPartCoverage| c.armour_value)
.unwrap_or_default()
< piece.armour_value
&& rng.random_bool(piece.coverage as f64)
{
current = Some(piece);
}
}
current
}
}

199
models/src/bundle/bonus.rs Normal file
View file

@ -0,0 +1,199 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use strum::Display;
use crate::bundle::player::BodyPart;
#[derive(Component, Debug, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponBonusType {
// Weapon passives
Berserk,
Conserve,
Expose,
Grace,
Powerful,
Specialist,
// Turn triggered passives
Empower,
Quicken,
// First turn effects
Assassinate,
// Additive status effects triggered by damaging hits
Cripple,
Demoralise,
Freeze,
Motivate,
Slow,
Toxin,
Weaken,
Wither,
// DOT status effects
Bleed,
Burning,
Lacerate,
Poison,
SevereBurning,
// Other status effects
Eviscerate,
Paralyse,
Schock,
Stun,
// Multi attack bonuses
Blindfire,
Fury,
DoubleTap,
Rage,
// Body part multipliers
Achilles,
Crusher,
Cupid,
Deadeye,
Roshambo,
Throttle,
// Attack nullification types
Homerun,
Parry,
}
#[derive(Component)]
pub struct BonusValue(pub f32);
#[derive(Bundle)]
pub struct WeaponBonusBundle {
pub bonus: WeaponBonusType,
pub value: BonusValue,
}
impl WeaponBonusBundle {
pub fn new(bonus: WeaponBonusType, value: f32) -> Self {
Self {
bonus,
value: BonusValue(value),
}
}
}
#[derive(Clone, Copy)]
pub enum TurnTriggeredBonus {
Empower,
Quicken,
}
#[derive(Clone, Copy)]
pub enum FirstTurnBonus {
Assassinate,
}
#[derive(Clone, Copy)]
pub enum MultiTurnBonus {
Blindfire,
Fury,
DoubleTap,
Rage,
}
impl MultiTurnBonus {
#[inline(always)]
pub fn counter_label(self) -> &'static str {
match self {
Self::Blindfire => "proc_blindfire",
Self::Fury => "proc_fury",
Self::DoubleTap => "proc_double_tap",
Self::Rage => "proc_rage",
}
}
}
#[derive(Clone, Copy)]
pub enum OpponentStatusEffect {
Cripple,
// TODO: implement for group fights
Demoralise,
Freeze,
Slow,
Toxin,
Weaken,
Wither,
}
#[derive(Clone, Copy)]
pub enum SelfStatusEffect {
Motivate,
}
#[derive(Clone, Copy)]
pub enum BonusPartDamageBonus {
Achilles,
Crusher,
Cupid,
Deadeye,
Roshambo,
Throttle,
}
impl BonusPartDamageBonus {
pub fn dmg_bonus(self, part: BodyPart, value: f32) -> Option<f32> {
match self {
Self::Achilles => match part {
BodyPart::LeftFoot | BodyPart::RightFoot => Some(value),
_ => None,
},
Self::Crusher => {
if part == BodyPart::Head {
Some(value)
} else {
None
}
}
Self::Cupid => {
if part == BodyPart::Heart {
Some(value)
} else {
None
}
}
Self::Deadeye => match part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => Some(value),
_ => None,
},
Self::Roshambo => {
if part == BodyPart::Groin {
Some(value)
} else {
None
}
}
Self::Throttle => {
if part == BodyPart::Throat {
Some(value)
} else {
None
}
}
}
}
}
#[derive(Clone, Copy, Debug, Display)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum ArmourBonusType {
Impregnable,
Impenetrable,
Insurmountable,
Invulnerable,
Imperviable,
Immmutable,
Irrepressible,
Kinetokinesis,
Impassable,
}

14
models/src/bundle/mod.rs Normal file
View file

@ -0,0 +1,14 @@
use bevy_ecs::component::Component;
pub mod armour;
pub mod bonus;
pub mod passive;
pub mod player;
pub mod stat;
pub mod weapon;
#[derive(Component, Debug, Default)]
pub struct Name(pub String);
#[derive(Component, Debug, Default, Clone, Copy)]
pub struct Id(pub usize);

View file

@ -0,0 +1,207 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use crate::bundle::player::BodyPart;
#[derive(Component, Default)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Merits {
pub life: u16,
pub crits: u16,
pub brawn: u16,
pub protection: u16,
pub sharpness: u16,
pub evasion: u16,
pub heavy_artillery_mastery: u16,
pub machine_gun_mastery: u16,
pub rifle_mastery: u16,
pub smg_mastery: u16,
pub shotgun_mastery: u16,
pub pistol_mastery: u16,
pub club_mastery: u16,
pub piercing_mastery: u16,
pub slashing_mastery: u16,
pub mechanical_mastery: u16,
pub temporary_mastery: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Education {
/// Gain a 1% damage bonus to all weapons
pub bio2350: bool,
/// Gain a 10% damage increase when hitting an opponent's throat
pub bio2380: bool,
/// Gain a 3% chance increase of achieving a critical hit
pub bio2410: bool,
/// Gain a 1% passive bonus to speed
pub cbt2790: bool,
/// Gain a +1.00 accuracy increase with Machine Guns
pub cbt2820: bool,
/// Gain a +1.00 accuracy increase with Submachine guns
pub cbt2830: bool,
/// Gain a +1.00 accuracy increase with Pistols
pub cbt2840: bool,
/// Gain a +1.00 accuracy increase with Rifles
pub cbt2850: bool,
/// Gain a +1.00 accuracy increase with Heavy Artillery
pub cbt2860: bool,
/// Gain a +1.00 accuracy increase with Shotguns
pub cbt2125: bool,
/// Gain a +1.00 accuracy increase with Temporary weapons
pub gen2116: bool,
/// Gain a 5% damage increase with Temporary weapons
pub gen2119: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2104: bool,
/// Gain a 1% passive bonus to speed
pub haf2105: bool,
/// Gain a 1% passive bonus to strength
pub haf2106: bool,
/// Gain a 2% passive bonus to strength
pub haf2107: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2108: bool,
/// Gain a 3% passive bonus to speed
pub haf2109: bool,
/// Gain a 10% damage increase with Japanese blade weapons
pub his2160: bool,
/// Gain a 2% bonus to all melee damage
pub his2170: bool,
/// Gain a 1% passive bonus to speed
pub mth2240: bool,
/// Gain a 1% passive bonus to speed
pub mth2250: bool,
/// Gain a 1% passive bonus to defense
pub mth2260: bool,
/// Gain a 2% passive bonus to defense
pub mth2320: bool,
/// Gain a 5% bonus to ammo conservation
pub mth2310: bool,
/// Gain a 20% bonus to ammo conservation
pub mth3330: bool,
/// Gain a 1% passive bonus to dexterity
pub psy2640: bool,
/// Gain a 2% passive bonus to dexterity
pub psy2650: bool,
/// Gain a 4% passive bonus to dexterity
pub psy2660: bool,
/// Gain an 8% passive bonus to dexterity
pub psy2670: bool,
/// Gain a 1% passive bonus to defense
pub def2710: bool,
/// Gain a 2% passive bonus to defense
pub def2730: bool,
/// Gain a 3% passive bonus to defense
pub def2740: bool,
/// Gain a 2% passive bonus to speed
pub def2750: bool,
/// Gain a 3% passive bonus to speed
pub def2760: bool,
/// Gain a 100% increase in damage dealt when using fists alone
pub def3770: bool,
/// Gain a 10% increase in steroid effectiveness
// NOTE: this effect is additive with the strip club perks
pub spt2480: bool,
/// Gain a 2% passive bonus to speed and strength
pub spt2490: bool,
/// Gain a 2% passive bonus to defense and dexterity
pub spt2500: bool,
}
impl Default for Education {
fn default() -> Self {
Self {
bio2350: true,
bio2380: true,
bio2410: true,
cbt2790: true,
cbt2820: true,
cbt2830: true,
cbt2840: true,
cbt2850: true,
cbt2860: true,
cbt2125: true,
gen2116: true,
gen2119: true,
haf2104: true,
haf2105: true,
haf2106: true,
haf2107: true,
haf2108: true,
haf2109: true,
his2160: true,
his2170: true,
mth2240: true,
mth2250: true,
mth2260: true,
mth2320: true,
mth2310: true,
mth3330: true,
psy2640: true,
psy2650: true,
psy2660: true,
psy2670: true,
def2710: true,
def2730: true,
def2740: true,
def2750: true,
def2760: true,
def3770: true,
spt2480: true,
spt2490: true,
spt2500: true,
}
}
}
#[derive(Component, Default)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct FactionUpgrades {
pub str: u16,
pub spd: u16,
pub def: u16,
pub dex: u16,
pub life: u16,
pub acc: u16,
pub dmg: u16,
pub side_effects: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub enum DrugCooldown {
Xanax,
Vicodin,
}
#[derive(Clone, Copy)]
pub enum EducationPartDamageBonus {
Bio2380,
}
impl EducationPartDamageBonus {
pub fn dmg_bonus(self, part: BodyPart) -> Option<f32> {
match part {
BodyPart::Throat => Some(0.10),
_ => None,
}
}
}
#[derive(Bundle, Default)]
pub(crate) struct PassiveBundle {
pub merits: Merits,
pub education: Education,
pub faction: FactionUpgrades,
}

184
models/src/bundle/player.rs Normal file
View file

@ -0,0 +1,184 @@
use bevy_ecs::{bundle::Bundle, component::Component, entity::Entity, message::Message};
use strum::Display;
use crate::bundle::{
Name,
bonus::BonusPartDamageBonus,
passive::EducationPartDamageBonus,
stat::{CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy},
weapon::WeaponSlot,
};
#[derive(Component)]
pub struct Attacker;
#[derive(Component)]
pub struct Defender;
#[derive(Component)]
pub struct Defeated;
#[derive(Component)]
pub struct Current;
#[derive(Component)]
pub struct CurrentTarget;
#[derive(Component, Default)]
pub struct Player;
#[derive(Component)]
pub struct Level(pub u16);
impl Default for Level {
fn default() -> Self {
Self(1)
}
}
#[derive(Component, Debug)]
pub struct MaxHealth(pub u16);
impl Default for MaxHealth {
fn default() -> Self {
Self(100)
}
}
#[derive(Component, Debug, Default)]
pub struct CombatTurns(pub u16);
#[derive(Component, Debug)]
pub struct Weapons {
pub primary: Option<Entity>,
pub secondary: Option<Entity>,
pub melee: Option<Entity>,
pub temporary: Option<Entity>,
pub fists: Entity,
pub kick: Entity,
}
#[derive(Component, Debug)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
pub enum PlayerStrategy {
AlwaysFists,
AlwaysKicks,
PrimaryMelee {
reload: bool,
},
InOrder {
order: Vec<WeaponSlot>,
reload: bool,
},
}
impl Default for PlayerStrategy {
fn default() -> Self {
Self::AlwaysFists
}
}
#[derive(Message)]
pub struct ChooseWeapon(pub Entity);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyPart {
LeftHand,
RightHand,
LeftArm,
RightArm,
LeftFoot,
RightFoot,
LeftLeg,
RightLeg,
Stomach,
Chest,
Groin,
Head,
Throat,
Heart,
}
impl std::fmt::Display for BodyPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LeftHand => write!(f, "Left hand"),
Self::RightHand => write!(f, "Right hand"),
Self::LeftArm => write!(f, "Left arm"),
Self::RightArm => write!(f, "Right arm"),
Self::LeftFoot => write!(f, "Left foot"),
Self::RightFoot => write!(f, "Right foot"),
Self::LeftLeg => write!(f, "Left leg"),
Self::RightLeg => write!(f, "Right leg"),
Self::Stomach => write!(f, "Stomach"),
Self::Chest => write!(f, "Chest"),
Self::Groin => write!(f, "Groin"),
Self::Head => write!(f, "Head"),
Self::Throat => write!(f, "Throat"),
Self::Heart => write!(f, "Heart"),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Display)]
pub enum FightEndType {
Victory,
Stalemate,
Loss,
}
#[derive(Component)]
pub enum PartDamageBonus {
Education(EducationPartDamageBonus),
WeaponBonus {
value: f32,
bonus: BonusPartDamageBonus,
},
}
#[derive(Message)]
pub enum HealthRestore {
Cauterise,
Serotonin { extra_effectiveness: f32 },
Bloodlust { dmg: u32, value: f32 },
}
impl PartDamageBonus {
pub fn dmg_bonus(&self, part: BodyPart) -> Option<f32> {
match self {
Self::Education(edu) => edu.dmg_bonus(part),
Self::WeaponBonus { value, bonus } => bonus.dmg_bonus(part, *value),
}
}
}
#[derive(Bundle)]
pub struct PlayerBundle {
pub name: Name,
pub player: Player,
pub level: Level,
pub crit_rate: SimpleStatBundle<CritRate>,
// TODO: since these two need to be tracked here anyways it might be preferable to shift all
// player specific passives here instead of tracking them on the weapons
pub acc_bonus: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub strategy: PlayerStrategy,
pub combat_turns: CombatTurns,
}
impl PlayerBundle {
pub fn new(name: impl ToString, level: u16, strategy: PlayerStrategy) -> Self {
Self {
name: Name(name.to_string()),
player: Player,
level: Level(level),
crit_rate: SimpleStatBundle::new(24),
acc_bonus: SimpleStatBundle::new(0.0),
dmg_bonus: SimpleStatBundle::new(0.0),
strategy,
combat_turns: Default::default(),
}
}
}

408
models/src/bundle/stat.rs Normal file
View file

@ -0,0 +1,408 @@
use std::marker::PhantomData;
use bevy_ecs::{bundle::Bundle, component::Component};
use crate::bundle::player::BodyPart;
pub trait SimpleStatMarker: Send + Sync + 'static {
type ValueType: Send + Sync + Copy + std::fmt::Display + 'static;
type BonusType: Send + Sync + Copy + 'static;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value
}
}
#[derive(Component)]
pub struct SimpleStatBaseline<Stat: SimpleStatMarker> {
pub value: Stat::ValueType,
marker: PhantomData<Stat>,
}
#[derive(Component)]
pub struct SimpleStatEffective<Stat: SimpleStatMarker> {
pub value: Stat::ValueType,
marker: PhantomData<Stat>,
}
#[derive(Component)]
pub struct SimpleStatBonus<Stat: SimpleStatMarker> {
pub label: &'static str,
pub value: Stat::BonusType,
marker: PhantomData<Stat>,
}
impl<Stat: SimpleStatMarker> SimpleStatBonus<Stat> {
pub fn new(label: &'static str, value: Stat::BonusType) -> Self {
Self {
label,
value,
marker: PhantomData,
}
}
}
#[derive(Component)]
pub struct SimpleStatSnapshot<Stat: SimpleStatMarker> {
pub value: Stat::ValueType,
pub marker: PhantomData<Stat>,
}
#[derive(Bundle)]
pub struct SimpleStatBundle<Stat: SimpleStatMarker> {
pub baseline: SimpleStatBaseline<Stat>,
pub effective: SimpleStatEffective<Stat>,
}
impl<Stat: SimpleStatMarker> SimpleStatBundle<Stat> {
pub fn new(value: Stat::ValueType) -> Self {
Self {
baseline: SimpleStatBaseline {
value,
marker: PhantomData,
},
effective: SimpleStatEffective {
value,
marker: PhantomData,
},
}
}
}
impl<Stat: SimpleStatMarker> Clone for SimpleStatEffective<Stat> {
fn clone(&self) -> Self {
*self
}
}
impl<Stat: SimpleStatMarker> Copy for SimpleStatEffective<Stat> where Stat::ValueType: Copy {}
#[derive(Default)]
pub struct CritRate;
impl SimpleStatMarker for CritRate {
type ValueType = u16;
type BonusType = u16;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value + bonus
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value - bonus
}
}
impl rand::distr::Distribution<BodyPart> for SimpleStatEffective<CritRate> {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> BodyPart {
if rng.random_ratio((self.value) as u32, 200) {
match rng.random_range(1..=10) {
1 => BodyPart::Heart,
2 => BodyPart::Throat,
_ => BodyPart::Heart,
}
} else {
match rng.random_range(1..=20) {
1 => BodyPart::LeftHand,
2 => BodyPart::RightHand,
3 => BodyPart::LeftArm,
4 => BodyPart::RightArm,
5 => BodyPart::LeftFoot,
6 => BodyPart::RightFoot,
7 | 8 => BodyPart::RightLeg,
9 | 10 => BodyPart::LeftLeg,
11..=15 => BodyPart::Chest,
16 => BodyPart::Groin,
_ => BodyPart::Stomach,
}
}
}
}
impl<Stat> std::ops::Add<&SimpleStatEffective<Stat>> for &SimpleStatEffective<Stat>
where
Stat: SimpleStatMarker,
Stat::ValueType: std::ops::Add<Stat::ValueType, Output = Stat::ValueType>,
{
type Output = SimpleStatEffective<Stat>;
fn add(self, rhs: &SimpleStatEffective<Stat>) -> Self::Output {
SimpleStatEffective {
value: self.value + rhs.value,
marker: PhantomData,
}
}
}
#[derive(Default)]
pub struct AmmoControl;
impl SimpleStatMarker for AmmoControl {
type ValueType = f32;
type BonusType = f32;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value + bonus
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value - bonus
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 100.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 100.0
}
}
#[derive(Default)]
pub struct DamageBonus;
impl SimpleStatMarker for DamageBonus {
type ValueType = f32;
type BonusType = f32;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value + bonus
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value - bonus
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 100.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 100.0
}
}
#[derive(Default)]
pub struct WeaponAccuracy;
impl SimpleStatMarker for WeaponAccuracy {
type ValueType = f32;
type BonusType = f32;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value + bonus
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value - bonus
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 50.0 + 50.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 50.0
}
}
#[derive(Default)]
pub struct ClipSize;
impl SimpleStatMarker for ClipSize {
type ValueType = u16;
type BonusType = f32;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
((value as f32) * bonus).round() as u16
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
((value as f32) / bonus).round() as u16
}
}
#[derive(Default)]
pub struct Clips;
impl SimpleStatMarker for Clips {
type ValueType = u16;
type BonusType = i16;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
((value as i16) + bonus) as u16
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
((value as i16) - bonus) as u16
}
}
#[derive(Default)]
pub struct Health;
impl SimpleStatMarker for Health {
type ValueType = u16;
type BonusType = u16;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value + bonus
}
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
value - bonus
}
}
#[derive(Debug, Clone, Copy)]
pub enum StatType {
Str,
Def,
Spd,
Dex,
}
pub trait StatMarker: Send + Sync + 'static {
fn stat_type() -> StatType;
}
#[derive(Debug, Default)]
pub struct Strength;
impl StatMarker for Strength {
fn stat_type() -> StatType {
StatType::Str
}
}
#[derive(Debug, Default)]
pub struct Defence;
impl StatMarker for Defence {
fn stat_type() -> StatType {
StatType::Def
}
}
#[derive(Debug, Default)]
pub struct Speed;
impl StatMarker for Speed {
fn stat_type() -> StatType {
StatType::Spd
}
}
#[derive(Debug, Default)]
pub struct Dexterity;
impl StatMarker for Dexterity {
fn stat_type() -> StatType {
StatType::Dex
}
}
#[derive(Component)]
pub struct BaselineStat<Stat: StatMarker> {
pub value: f32,
pub marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> Default for BaselineStat<Stat> {
fn default() -> Self {
Self {
value: 10.0,
marker: PhantomData,
}
}
}
#[derive(Component, Default)]
pub struct EffectiveStat<Stat: StatMarker> {
pub value: f32,
pub marker: PhantomData<Stat>,
}
#[derive(Component)]
pub struct AdditiveBonuses<Stat: StatMarker> {
pub factor: f32,
pub marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> Default for AdditiveBonuses<Stat> {
fn default() -> Self {
Self {
factor: 1.0,
marker: PhantomData,
}
}
}
#[derive(Component)]
pub struct MultiplicativeBonuses<Stat: StatMarker> {
pub factor: f32,
pub marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> Default for MultiplicativeBonuses<Stat> {
fn default() -> Self {
Self {
factor: 1.0,
marker: PhantomData,
}
}
}
#[derive(Bundle, Default)]
pub struct StatBundle<Stat: StatMarker> {
pub baseline: BaselineStat<Stat>,
pub additive: AdditiveBonuses<Stat>,
pub multiplicative: MultiplicativeBonuses<Stat>,
pub effective: EffectiveStat<Stat>,
}
#[derive(Component)]
pub struct StatSnapshot<Stat: StatMarker> {
pub additive_bonuses: f32,
pub multiplicative_bonuses: f32,
pub effective: f32,
pub marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> StatBundle<Stat> {
pub fn new(baseline: f32) -> Self {
Self {
baseline: BaselineStat {
value: baseline,
marker: PhantomData,
},
effective: EffectiveStat {
value: baseline,
marker: PhantomData,
},
additive: AdditiveBonuses {
factor: 1.0,
marker: PhantomData,
},
multiplicative: MultiplicativeBonuses {
factor: 1.0,
marker: PhantomData,
},
}
}
}
#[derive(Component)]
pub struct AdditiveBonus<Stat: StatMarker> {
pub label: &'static str,
pub value: f32,
marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> AdditiveBonus<Stat> {
pub fn new(label: &'static str, value: f32) -> Self {
Self {
label,
value,
marker: PhantomData,
}
}
}
#[derive(Component)]
pub struct MultiplicativeBonus<Stat: StatMarker> {
pub label: &'static str,
pub value: f32,
marker: PhantomData<Stat>,
}
impl<Stat: StatMarker> MultiplicativeBonus<Stat> {
pub fn new(label: &'static str, value: f32) -> Self {
Self {
label,
value,
marker: PhantomData,
}
}
}

230
models/src/bundle/weapon.rs Normal file
View file

@ -0,0 +1,230 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use strum::Display;
use crate::{
bundle::{
Name,
stat::{
AmmoControl, ClipSize, Clips, CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy,
},
},
dto::weapon::{WeaponAmmo, WeaponDto},
};
#[derive(Component)]
pub struct Usable;
#[derive(Component)]
pub struct Weapon;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponSlot {
Primary,
Secondary,
Melee,
Temporary,
Fists,
Kick,
}
#[derive(Component, Debug, Clone, Copy)]
pub enum WeaponVerb {
Hit,
Kicked,
Fired,
Threw,
Exploded,
}
#[derive(Component, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponCategory {
HeavyArtillery,
MachineGun,
Rifle,
Smg,
Shotgun,
Pistol,
Clubbing,
Piercing,
Slashing,
Mechanical,
Temporary,
HandToHand,
}
#[derive(Component, Debug)]
pub struct DamageStat(pub f32);
#[derive(Component)]
pub struct Japanese;
#[derive(Component)]
pub struct Ammo(pub u16);
#[derive(Component)]
pub struct RateOfFire(pub [u16; 2]);
#[derive(Component)]
pub struct NeedsReload;
#[derive(Component, Default)]
pub struct NonTargeted;
#[derive(Component, Default)]
pub struct Temporary;
#[derive(Component)]
pub struct Uses(pub u16);
impl Default for Uses {
fn default() -> Self {
Self(1)
}
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponMod {
ReflexSight,
HolographicSight,
AcogSight,
ThermalSight,
Laser1mw,
Laser5mw,
Laser30mw,
Laser100mw,
SmallSuppressor,
StandardSuppressor,
LargeSuppressor,
ExtendedMags,
HighCapacityMags,
ExtraClip,
ExtraClip2,
AdjustableTrigger,
HairTrigger,
Bipod,
Tripod,
CustomGrip,
SkeetChoke,
ImprovedChoke,
FullChoke,
RecoilPad,
StandardBrake,
HeavyDutyBrake,
TacticalBrake,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Clone, Copy)]
pub enum TurnTriggeredMod {
Bipod,
Tripod,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Component, Debug, Clone, Copy, Display)]
pub enum DebuffingTemp {
TearGas,
SmokeGrenade,
PepperSpray,
ConcussionGrenade,
FlashGrenade,
}
#[derive(Component, Debug, Clone, Copy, Display)]
pub enum BuffingTemp {
Serotonin,
Tyrosine,
Melatonin,
Epinephrine,
}
#[derive(Component)]
pub struct EquippedMods(pub Vec<WeaponMod>);
#[derive(Component)]
pub struct Experience(pub f32);
#[derive(Bundle)]
pub struct WeaponBundle {
pub usable: Usable,
pub weapon: Weapon,
pub name: Name,
pub verb: WeaponVerb,
pub slot: WeaponSlot,
}
#[derive(Bundle)]
pub struct DamagingWeaponBundle {
pub crit_rate: SimpleStatBundle<CritRate>,
pub dmg: DamageStat,
pub acc: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub equipped_mods: EquippedMods,
pub experience: Experience,
pub category: WeaponCategory,
}
#[derive(Bundle)]
pub struct AmmoWeaponBundle {
pub ammo: Ammo,
pub clips: SimpleStatBundle<Clips>,
pub clip_size: SimpleStatBundle<ClipSize>,
pub rate_of_fire: RateOfFire,
pub ammo_control: SimpleStatBundle<AmmoControl>,
}
impl WeaponBundle {
pub fn new(name: String, verb: WeaponVerb, slot: WeaponSlot) -> Self {
Self {
usable: Usable,
weapon: Weapon,
name: Name(name),
verb,
slot,
}
}
pub fn fists() -> Self {
Self::new("Fists".to_owned(), WeaponVerb::Hit, WeaponSlot::Fists)
}
pub fn kick() -> Self {
Self::new("Kick".to_owned(), WeaponVerb::Kicked, WeaponSlot::Kick)
}
}
impl AmmoWeaponBundle {
pub fn new(weapon_ammo: &WeaponAmmo) -> Self {
Self {
ammo: Ammo(weapon_ammo.clip_size),
clips: SimpleStatBundle::new(2),
clip_size: SimpleStatBundle::new(weapon_ammo.clip_size),
rate_of_fire: RateOfFire(weapon_ammo.rate_of_fire),
ammo_control: SimpleStatBundle::new(0.0),
}
}
}
impl DamagingWeaponBundle {
pub fn new(dto: &WeaponDto) -> Self {
Self {
crit_rate: SimpleStatBundle::new(0),
dmg: DamageStat(dto.dmg.unwrap_or(dto.base_dmg) / 10.0),
acc: SimpleStatBundle::new((dto.acc.unwrap_or(dto.base_acc) - 50.0) / 50.0),
dmg_bonus: SimpleStatBundle::new(0.0),
equipped_mods: EquippedMods(dto.mods.iter().filter_map(|m| *m).collect()),
experience: Experience(dto.experience.unwrap_or_default()),
category: dto.cat,
}
}
}

2122
models/src/dto/armour.rs Normal file

File diff suppressed because it is too large Load diff

32
models/src/dto/metrics.rs Normal file
View file

@ -0,0 +1,32 @@
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
#[derive(Debug, Clone)]
pub enum EntityInfo {
Player {
name: String,
id: usize,
is_attacker: bool,
},
Weapon {
name: String,
owner: usize,
id: usize,
},
Global,
}
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Counter {
pub value: u64,
pub entity: EntityInfo,
pub label: &'static str,
}
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Histogram {
pub values: Vec<u32>,
pub entity: EntityInfo,
pub label: &'static str,
}

14
models/src/dto/mod.rs Normal file
View file

@ -0,0 +1,14 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::bundle::Id;
pub mod armour;
pub mod metrics;
pub mod player;
pub mod weapon;
static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn draw_id() -> Id {
Id(ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}

107
models/src/dto/player.rs Normal file
View file

@ -0,0 +1,107 @@
use bevy_ecs::prelude::*;
use crate::{
bundle::{
armour::PlayerArmour,
passive::{DrugCooldown, Education, FactionUpgrades, Merits, PassiveBundle},
player::{PlayerBundle, PlayerStrategy, Weapons},
stat::{Defence, Dexterity, Speed, StatBundle, Strength},
},
dto::{armour::ArmourDto, draw_id, weapon::WeaponDto},
};
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct EquippedWeapons {
pub primary: Option<WeaponDto>,
pub secondary: Option<WeaponDto>,
pub melee: Option<WeaponDto>,
pub temporary: Option<WeaponDto>,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[derive(Default)]
pub struct EquippedArmour {
pub helmet: Option<ArmourDto>,
pub body: Option<ArmourDto>,
pub pants: Option<ArmourDto>,
pub gloves: Option<ArmourDto>,
pub boots: Option<ArmourDto>,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Stats {
pub str: f32,
pub def: f32,
pub spd: f32,
pub dex: f32,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct PlayerDto {
pub name: String,
pub level: u16,
pub stats: Stats,
pub merits: Option<Merits>,
pub education: Option<Education>,
pub weapons: EquippedWeapons,
pub armour: EquippedArmour,
pub faction: Option<FactionUpgrades>,
pub drug: Option<DrugCooldown>,
pub strategy: PlayerStrategy,
}
impl PlayerDto {
pub fn spawn(self, world: &mut World) -> EntityWorldMut<'_> {
let primary = self.weapons.primary.map(|p| p.spawn(world));
let secondary = self.weapons.secondary.map(|s| s.spawn(world));
let melee = self.weapons.melee.map(|m| m.spawn(world));
let temporary = self.weapons.temporary.map(|m| m.spawn(world));
let fists = WeaponDto::FISTS.spawn(world);
let kick = WeaponDto::KITCHEN_KNIFE.spawn(world);
let head = self.armour.helmet.map(|a| a.spawn(world));
let torso = self.armour.body.map(|a| a.spawn(world));
let legs = self.armour.pants.map(|a| a.spawn(world));
let hands = self.armour.gloves.map(|a| a.spawn(world));
let feet = self.armour.boots.map(|a| a.spawn(world));
let mut commands = world.spawn(draw_id());
commands.insert((
PlayerBundle::new(self.name, self.level, self.strategy),
StatBundle::<Strength>::new(self.stats.str),
StatBundle::<Defence>::new(self.stats.def),
StatBundle::<Speed>::new(self.stats.spd),
StatBundle::<Dexterity>::new(self.stats.dex),
));
let education = self.education.unwrap_or_default();
let merits = self.merits.unwrap_or_default();
let faction = self.faction.unwrap_or_default();
commands.insert(PassiveBundle {
education,
merits,
faction,
});
commands.insert(Weapons {
primary,
secondary,
melee,
temporary,
kick,
fists,
});
commands.insert(PlayerArmour {
torso,
head,
legs,
feet,
hands,
});
commands
}
}

5609
models/src/dto/weapon.rs Normal file

File diff suppressed because it is too large Load diff

178
models/src/hierarchy.rs Normal file
View file

@ -0,0 +1,178 @@
use bevy_ecs::{
prelude::*,
system::{Command, EntityCommands},
};
#[derive(Component, Debug, Clone, Copy)]
pub struct Parent(Entity);
impl Parent {
pub fn get(&self) -> Entity {
self.0
}
}
#[derive(Component, Debug, Clone, Default)]
pub struct Children(Vec<Entity>);
impl Children {
pub fn get(&self) -> &[Entity] {
&self.0
}
}
struct AddChild {
parent: Entity,
child: Entity,
}
impl Command for AddChild {
fn apply(self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
if let Some(mut children) = parent.get_mut::<Children>() {
children.0.push(self.child);
} else {
parent.insert(Children(vec![self.child]));
}
}
}
struct AddChildren {
parent: Entity,
children: Vec<Entity>,
}
impl Command for AddChildren {
fn apply(mut self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
if let Some(mut children) = parent.get_mut::<Children>() {
children.0.append(&mut self.children);
} else {
parent.insert(Children(self.children));
}
}
}
struct RemoveChild {
parent: Entity,
child: Entity,
}
impl Command for RemoveChild {
fn apply(self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
let mut children = parent
.get_mut::<Children>()
.expect("Parent component has no children");
children.0.retain(|child| *child != self.child);
}
}
pub trait HierarchyBuilder {
fn add_child(&mut self, child: Entity) -> &mut Self;
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self;
fn remove_child(&mut self, child: Entity) -> &mut Self;
fn set_parent(&mut self, parent: Entity) -> &mut Self;
}
impl<'a> HierarchyBuilder for EntityCommands<'a> {
fn add_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id();
self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent));
self
}
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self {
let children = children.as_ref();
let parent = self.id();
self.commands().queue(AddChildren {
parent,
children: children.to_owned(),
});
self.commands().insert_batch(
children
.iter()
.map(|e| (*e, Parent(parent)))
.collect::<Vec<_>>(),
);
self
}
fn remove_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id();
self.commands().queue(RemoveChild { parent, child });
self.commands().entity(child).remove::<Parent>();
self
}
fn set_parent(&mut self, parent: Entity) -> &mut Self {
let child = self.id();
self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent));
self
}
}
impl<'w> HierarchyBuilder for EntityWorldMut<'w> {
fn add_child(&mut self, child: Entity) -> &mut Self {
let parent_id = self.id();
unsafe {
self.world_mut()
.entity_mut(child)
.insert(Parent(parent_id))
.update_location();
}
if let Some(mut children) = self.get_mut::<Children>() {
children.0.push(child);
self
} else {
self.insert(Children(vec![child]))
}
}
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self {
let parent_id = self.id();
unsafe {
for child in children.as_ref() {
self.world_mut()
.entity_mut(*child)
.insert(Parent(parent_id))
.update_location();
}
}
if let Some(mut old_children) = self.get_mut::<Children>() {
old_children.0.append(&mut children.as_ref().to_owned());
self
} else {
self.insert(Children(children.as_ref().to_owned()))
}
}
fn remove_child(&mut self, child: Entity) -> &mut Self {
unsafe {
self.world_mut()
.entity_mut(child)
.remove::<Parent>()
.update_location();
}
if let Some(mut children) = self.get_mut::<Children>() {
children.0.retain(|c| *c != child);
}
self
}
fn set_parent(&mut self, parent: Entity) -> &mut Self {
let child_id = self.id();
unsafe {
self.world_mut()
.entity_mut(parent)
.add_child(child_id)
.update_location()
}
self
}
}

3
models/src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod bundle;
pub mod dto;
// pub mod hierarchy;