Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions protocol/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use tokio::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};

use crate::{ReadCwData, WriteCwData};
use crate::packet::common::item::Kind;
use crate::utils::constants::SIZE_BLOCK;
use crate::utils::io_extensions::{ReadArbitrary, WriteArbitrary};

Expand Down Expand Up @@ -47,6 +48,13 @@ pub fn max_level_of(level: i32) -> i32 {
(level..=i32::from(i16::MAX)).take_while(|&l| power_of(l) == power).last().unwrap_or(level)
}

#[must_use]
pub fn max_valid_item_level(kind: Kind, player_level: i16) -> i16 {
if kind.uses_power() { max_level_of(player_level.into()) as i16 }
else if kind.uses_level() { player_level }
else { 1 }
}

#[must_use]
pub fn maximum_experience_of(level: i32) -> i32 {
// order of operations matches cubeworld's rounding behaviour
Expand Down
50 changes: 50 additions & 0 deletions protocol/src/utils/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,56 @@ pub const PLAYABLE_RACES: [Race; 16] = [
UndeadFemale
];

pub const VANILLA_PETS: [Race; 47] = [
Collie,
Alpaca,
AlpacaBrown,
Turtle,
Terrier,
TerrierScottish,
Cat,
Pig,
Sheep,
Bunny,
Porcupine,
SlimeGreen,
SlimePink,
SlimeYellow,
SlimeBlue,
Monkey,
Hornet,
Crow,
Chicken,
Seagull,
Parrot,
Bat,
Fly,
Midge,
Mosquito,
RunnerPlain,
RunnerLeaf,
RunnerSnow,
RunnerDesert,
Peacock,
Duckbill,
Crocodile,
Spitter,
Mole,
Biter,
Squirrel,
Raccoon,
Owl,
Penguin,
Horse,
Camel,
BeetleDark,
BeetleFire,
BeetleSnout,
BeetleLemon,
Crab,
Bumblebee
];

pub const TWO_HANDED_WEAPONS: [Weapon; 10] = [
Longsword,
Bow,
Expand Down
4 changes: 2 additions & 2 deletions server/src/addon/command_manager/commands/give.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use protocol::packet::world_update::Pickup;
use protocol::packet::WorldUpdate;
use protocol::utils::constants::materials::by_item_kind;
use protocol::utils::constants::rarity::*;
use protocol::utils::power_of;
use protocol::utils::{max_valid_item_level, power_of};
use tap::Pipe;

use crate::addon::command_manager::{Command, CommandResult};
Expand Down Expand Up @@ -45,7 +45,7 @@ impl Command for super::Give {

let valid_materials = by_item_kind(item.kind);
if item.kind.uses_rarity() {item.rarity = LEGENDARY} else {item.rarity = NORMAL}
if item.kind.uses_level() {item.level = caller.character.read().await.level as i16} else {item.level = 1}
item.level = max_valid_item_level(item.kind, caller.character.read().await.level as i16);
if !valid_materials.is_empty() {item.material = valid_materials[0]}

for param in params {
Expand Down
137 changes: 109 additions & 28 deletions server/src/addon/events/legacy_koth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ use std::sync::Arc;
use std::time::Duration;

use config::{Config, ConfigError};
use rand::random;
use strum::IntoEnumIterator;
use rand::{random_range, rng};
use rand::seq::IndexedRandom;
use serde::Deserialize;
use tap::Tap;
use tokio::sync::RwLock;
use tokio::time::sleep;

use protocol::nalgebra::Point3;
use protocol::packet::{ChatMessageFromServer, CreatureUpdate, WorldUpdate};
use protocol::packet::common::{CreatureId, EulerAngles, Hitbox, Item, Race, item::{Kind, kind}};
use protocol::packet::creature_update::Affiliation;
use protocol::packet::common::{CreatureId, EulerAngles, Hitbox, Item, Race, item::{Kind, Material, kind}};
use protocol::packet::creature_update::{Affiliation, Occupation};
use protocol::packet::world_update::{Mission, Pickup, sound};
use protocol::packet::world_update::mission::{Objective, State};
use protocol::utils::constants::{SIZE_BLOCK, SIZE_ZONE, SIZE_SECTOR};
use protocol::utils::constants::{SIZE_BLOCK, SIZE_ZONE, SIZE_SECTOR, VANILLA_PETS};
use protocol::utils::constants::materials::by_item_kind;
use protocol::utils::constants::rarity::{NORMAL, RARE, EPIC, LEGENDARY};
use protocol::utils::constants::rarity::{NORMAL, UNCOMMON, RARE, EPIC, LEGENDARY};
use protocol::utils::max_valid_item_level;

use crate::addon::events::utils::{appearance_invisible, config_fallback, config_optional, creatures_circular, is_in_zone, pick_from, NAME_OVERFLOW, RENDER_DISTANCE_CREATURE};
use crate::addon::play_sound_at_player;
Expand All @@ -33,10 +35,6 @@ const TORCHES_ID: i64 = 75000;
const REWARD_POINTS: i32 = 10000;
const REWARD_THRESHOLDS: [i32; 4] = [20, 40, 60, 80];

const REWARD_WEAPON_ODDS: f32 = 0.30;
const REWARD_ARMOR_ODDS: f32 = 0.30;
const REWARD_SPIRIT_ODDS: f32 = 0.35;

#[derive(Debug)]
pub struct LegacyKoth {
points: RwLock<HashMap<CreatureId, i32>>,
Expand All @@ -50,7 +48,9 @@ pub struct LegacyKoth {
kill_king_points: i32,
kill_king_xp: i32,
kill_points: i32,
kill_xp: i32
kill_xp: i32,
loot: LootWeights,
rarity: RarityWeights
}

impl LegacyKoth {
Expand All @@ -63,6 +63,11 @@ impl LegacyKoth {
let reward_frequency: i32 = config_fallback(config, "legacykoth.reward_frequency", 420_i32)?;
let king_reward_frequency: i32 = config_fallback(config, "legacykoth.king_reward_frequency", 180_i32)?;

let loot: LootWeights = config_fallback(config, "legacykoth.loot", LootWeights::default())?;
let rarity: RarityWeights = config_fallback(config, "legacykoth.rarity", RarityWeights::default())?;
if loot.table().iter().all(|&(_, weight)| weight == 0) { return Err(ConfigError::Message("legacykoth.loot needs a non-zero weight".into())) }
if rarity.table().iter().all(|&(_, weight)| weight == 0) { return Err(ConfigError::Message("legacykoth.rarity needs a non-zero weight".into())) }

Ok(Self {
points: RwLock::new(HashMap::new()),
center,
Expand All @@ -75,7 +80,9 @@ impl LegacyKoth {
kill_king_points: config_fallback(config, "legacykoth.kill_king_points", 500_i32)?,
kill_king_xp: config_fallback(config, "legacykoth.kill_king_xp", 20_i32)?,
kill_points: config_fallback(config, "legacykoth.kill_points", 200_i32)?,
kill_xp: config_fallback(config, "legacykoth.kill_xp", 10_i32)?
kill_xp: config_fallback(config, "legacykoth.kill_xp", 10_i32)?,
loot,
rarity
})
}
}
Expand Down Expand Up @@ -194,35 +201,109 @@ async fn handle_points(player: &Player, threshold: Option<i32>, reward: bool) {
}

async fn give_reward(player: &Player) {
let level = player.character.read().await.level as i16;
let pickup = Pickup { interactor: player.id, item: reward_item(level) };
let (level, occupation) = {
let character = player.character.read().await;
(character.level as i16, character.occupation)
};
let pickup = Pickup { interactor: player.id, item: reward_item(&SERVER.addons.events.legacy_koth, level, occupation) };
play_sound_at_player(player, sound::Kind::Missioncomplete, 0.62, 1.0).await;
player.send_ignoring(&WorldUpdate::from(pickup)).await;
}

fn reward_item(level: i16) -> Item {
#[derive(Debug, Clone, Copy)]
enum Loot { Weapon, Armor, Amulet, Ring, Leftovers, Spirit, Lamp, Pet }

#[derive(Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct LootWeights { weapon: u32, armor: u32, amulet: u32, ring: u32, leftovers: u32, spirit: u32, lamp: u32, pet: u32 }

impl Default for LootWeights {
fn default() -> Self {
Self { weapon: 25, armor: 25, amulet: 7, ring: 7, leftovers: 4, spirit: 25, lamp: 2, pet: 5 }
}
}

impl LootWeights {
const fn table(&self) -> [(Loot, u32); 8] {
[(Loot::Weapon, self.weapon), (Loot::Armor, self.armor), (Loot::Amulet, self.amulet), (Loot::Ring, self.ring),
(Loot::Leftovers, self.leftovers), (Loot::Spirit, self.spirit), (Loot::Lamp, self.lamp), (Loot::Pet, self.pet)]
}
}

#[derive(Debug, Deserialize)]
#[serde(default, deny_unknown_fields)]
struct RarityWeights { normal: u32, uncommon: u32, rare: u32, epic: u32, legendary: u32 }

impl Default for RarityWeights {
fn default() -> Self {
Self { normal: 0, uncommon: 0, rare: 0, epic: 1, legendary: 1 }
}
}

impl RarityWeights {
const fn table(&self) -> [(u8, u32); 5] {
[(NORMAL, self.normal), (UNCOMMON, self.uncommon), (RARE, self.rare), (EPIC, self.epic), (LEGENDARY, self.legendary)]
}
}

fn pick_weighted<T: Copy>(table: &[(T, u32)]) -> Option<T> {
table.choose_weighted(&mut rng(), |(_, weight)| *weight).ok().map(|(value, _)| *value)
}

type ClassWeapons = &'static [(kind::Weapon, &'static [Material])];

fn class_gear(occupation: Occupation) -> Option<(Material, ClassWeapons)> {
use kind::Weapon::{Axe, Boomerang, Bow, Bracelet, Crossbow, Dagger, Fist, Greataxe, Greatmace, Greatsword, Longsword, Mace, Shield, Staff, Sword, Wand};
use Material::{Cotton, Gold, Iron, Linen, Silk, Silver, Wood};

match occupation {
Occupation::Warrior => Some((Iron, &[(Sword, &[Iron]), (Axe, &[Iron]), (Mace, &[Iron]), (Shield, &[Iron]),
(Greatsword, &[Iron]), (Greataxe, &[Iron]), (Greatmace, &[Iron, Wood])])),
Occupation::Ranger => Some((Linen, &[(Bow, &[Wood]), (Crossbow, &[Wood]), (Boomerang, &[Wood])])),
Occupation::Mage => Some((Silk, &[(Wand, &[Wood]), (Staff, &[Wood]), (Bracelet, &[Gold, Silver])])),
Occupation::Rogue => Some((Cotton, &[(Longsword, &[Iron]), (Dagger, &[Iron]), (Fist, &[Iron])])),
_ => None
}
}

fn reward_item(lkoth: &LegacyKoth, level: i16, occupation: Occupation) -> Item {
let mut item = Item::default();

item.kind = match random::<f32>() {
roll if roll < REWARD_WEAPON_ODDS
=> Kind::Weapon(pick_from(&kind::Weapon::iter().collect::<Vec<_>>())),
roll if roll < REWARD_WEAPON_ODDS + REWARD_ARMOR_ODDS
=> pick_from(&[Kind::Chest, Kind::Gloves, Kind::Boots, Kind::Shoulder]),
roll if roll < REWARD_WEAPON_ODDS + REWARD_ARMOR_ODDS + REWARD_SPIRIT_ODDS
=> Kind::Resource(kind::Resource::Spirit),
_ => Kind::Pet(pick_from(&Race::iter().collect::<Vec<_>>()))
let gear = class_gear(occupation);
let table = lkoth.loot.table().map(|(loot, weight)| match loot {
Loot::Weapon | Loot::Armor if gear.is_none() => (loot, 0),
_ => (loot, weight)
});

(item.kind, item.material) = match (pick_weighted(&table), gear) {
(Some(Loot::Weapon), Some((_, weapons))) => {
let (weapon, materials) = pick_from(weapons);
(Kind::Weapon(weapon), pick_from(materials))
}
(Some(Loot::Armor), Some((armor, _)))
=> (pick_from(&[Kind::Chest, Kind::Gloves, Kind::Boots, Kind::Shoulder]), armor),
(loot, _) => {
let kind = match loot {
Some(Loot::Amulet) => Kind::Amulet,
Some(Loot::Ring) => Kind::Ring,
Some(Loot::Leftovers) => Kind::Leftovers,
Some(Loot::Lamp) => Kind::Lamp,
Some(Loot::Pet) => Kind::Pet(pick_from(&VANILLA_PETS)),
_ => Kind::Resource(kind::Resource::Spirit)
};
(kind, pick_from(by_item_kind(kind)))
}
};

item.rarity = match item.kind {
Kind::Weapon(_) | Kind::Chest | Kind::Gloves | Kind::Boots | Kind::Shoulder
=> pick_from(&[EPIC, LEGENDARY]),
Kind::Weapon(_) | Kind::Chest | Kind::Gloves | Kind::Boots | Kind::Shoulder | Kind::Amulet | Kind::Ring | Kind::Leftovers
=> pick_weighted(&lkoth.rarity.table()).expect("validated at startup"),
Kind::Resource(kind::Resource::Spirit) => RARE,
_ => if item.kind.uses_rarity() { LEGENDARY } else { NORMAL }
};

item.material = pick_from(by_item_kind(item.kind));
item.level = if item.kind.uses_level() { level } else { 1 };
item.seed = if item.uses_seed() { random() } else { 0 };
item.level = max_valid_item_level(item.kind, level);
item.seed = if item.uses_seed() { random_range(0..=i32::MAX) } else { 0 };

item
}
Expand Down
10 changes: 2 additions & 8 deletions server/src/addon/events/shop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use protocol::packet::WorldUpdate;
use protocol::packet::CreatureUpdate;
use protocol::utils::constants::{materials, SIZE_BLOCK, SIZE_ZONE};
use protocol::utils::constants::rarity::*;
use protocol::utils::max_level_of;
use protocol::utils::max_valid_item_level;

use crate::addon::events::utils::{appearance_invisible, config_fallback, config_optional, creatures_circular, NAME_OVERFLOW};
use crate::addon::play_sound_at_player;
Expand Down Expand Up @@ -306,7 +306,7 @@ fn item_validation(state: &mut State, item: &mut Item, player_level: i16) -> Res
State::Rarity => {if item.kind.uses_rarity() { return Ok(()) }
item.rarity = NORMAL;
*state = State::Model;}
State::Model => {item.level = item_level(item.kind, player_level);
State::Model => {item.level = max_valid_item_level(item.kind, player_level);
if item.uses_models() { return Ok(()) }
item.seed = 0;
*state = State::Stat;}
Expand All @@ -317,12 +317,6 @@ fn item_validation(state: &mut State, item: &mut Item, player_level: i16) -> Res
}
}

fn item_level(kind: Kind, player_level: i16) -> i16 {
if kind.uses_power() { max_level_of(player_level.into()) as i16 }
else if kind.uses_level() { player_level }
else { 1 }
}

fn item_name(kind: Kind) -> String {
let subtype = subtype_names(kind);
if subtype.is_empty() { kind.to_string() } else { subtype }
Expand Down