Add support for modular weapons

This commit is contained in:
Jeff 2024-07-16 17:38:10 -04:00
parent c52613fa20
commit 19d0aa7e44
4 changed files with 301 additions and 120 deletions

View File

@ -101,11 +101,17 @@ announcement = "I love cheese! I am at {location}."
# values are the price in coins. You may type in-game "/give_item common.items." and press Tab to
# explore the item definition IDs. Then just leave off the "common.items." part in this file.
[buy_prices]
[buy_prices.simple]
"food.cheese" = 50
[sell_prices]
[sell_prices.simple]
"consumable.potion_minor" = 150
[sell_prices.modular]
material = "mineral.ingot.orichalcum"
primary = "sword.greatsword"
seconday = "sword.long"
price = 45_000
```
### Running

View File

@ -1,63 +1,20 @@
position = [17720.0, 14951.0, 237.0]
orientation = 0
[buy_prices]
[buy_prices.simple]
"food.cheese" = 50
[sell_prices]
# Armor
"armor.misc.neck.carcanet_of_wrath" = 20_000
[[buy_prices.modular]]
material = "Iron"
primary = "sword.greatsword"
secondary = "sword.long"
price = 1000
## Boreal Armor
"armor.boreal.back" = 250_000
"armor.boreal.belt" = 250_000
"armor.boreal.chest" = 250_000
"armor.boreal.foot" = 250_000
"armor.boreal.hand" = 250_000
"armor.boreal.pants" = 250_000
"armor.boreal.shoulder" = 250_000
"armor.misc.head.boreal_warhelm" = 450_000
# Hats
"armor.misc.head.cat_capuche" = 750_000
"armor.misc.head.hare_hat" = 150_000
"armor.misc.head.winged_coronet" = 40_000
"calendar.christmas.armor.misc.head.woolly_wintercap" = 250_000
# Crafting
"crafting_ing.alkahest" = 6_000
"crafting_ing.brinestone" = 2_000
"crafting_ing.coral_branch" = 1_000
"crafting_ing.hide.dragon_scale" = 5_000
"crafting_ing.dwarven_battery" = 40_000
"log.eldwood" = 3_000
"mineral.ingot.orichalcum" = 8_000
"mineral.ore.ancient_gold" = 10_000
# Potions
[sell_prices.simple]
"consumable.potion_minor" = 150
# Gliders
"glider.skullgrin" = 20_000
# Recipes
"recipes.armor.brinestone" = 8_000
"recipes.equipment.advanced" = 8_000
"recipes.unique.mindflayer_spellbag" = 10_000
"recipes.unique.abyssal_gorget" = 6_000
# Instruments
"tool.instruments.icy_talharpa" = 500_000
"tool.instruments.steeltonguedrum" = 300_000
# Legendary Weapons
"weapons.axe.parashu" = 100_000
"weapons.sword.caladbolg" = 150_000
"weapons.staff.laevateinn" = 60_000
"weapons.hammer.mjolnir" = 150_000
"weapons.sceptre.caduceus" = 150_000
# Lanterns
"boss_drops.lantern" = 40_000 # Magic Lantern
"lantern.blue_0" = 20_000
"lantern.geode_purp" = 20_000
[[sell_prices.modular]]
material = "Iron"
primary = "sword.greatsword"
secondary = "sword.long"
price = 1000

View File

@ -16,7 +16,10 @@ use veloren_common::{
clock::Clock,
comp::{
invite::InviteKind,
item::{ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc, ItemI18n, MaterialStatManifest},
item::{
modular, ItemBase, ItemDef, ItemDefinitionId, ItemDefinitionIdOwned, ItemDesc,
ItemI18n, MaterialStatManifest,
},
slot::InvSlotId,
tool::AbilityMap,
ChatType, ControllerInputs, Item, Ori, Pos,
@ -30,7 +33,10 @@ use veloren_common::{
};
use veloren_common_net::sync::WorldSyncExt;
use crate::PriceList;
const COINS: &str = "common.items.utility.coins";
const COINS_ID: ItemDefinitionId = ItemDefinitionId::Simple(Cow::Borrowed(COINS));
/// An active connection to the Veloren server that will attempt to run every time the `tick`
/// function is called.
@ -50,8 +56,8 @@ pub struct Bot {
item_i18n: ItemI18n,
localization: LocalizationHandle,
buy_prices: HashMap<String, u32>,
sell_prices: HashMap<String, u32>,
buy_prices: PriceList,
sell_prices: PriceList,
trade_mode: TradeMode,
previous_offer: Option<(HashMap<InvSlotId, u32>, HashMap<InvSlotId, u32>)>,
@ -72,8 +78,8 @@ impl Bot {
password: &str,
character: &str,
admins: Vec<String>,
buy_prices: HashMap<String, u32>,
sell_prices: HashMap<String, u32>,
buy_prices: PriceList,
sell_prices: PriceList,
position: Option<[f32; 3]>,
orientation: Option<f32>,
announcement: Option<String>,
@ -547,9 +553,9 @@ impl Bot {
inventories.get(them).ok_or("Failed to find inventory")?,
);
let coins = ItemDefinitionIdOwned::Simple(COINS.to_string());
let get_my_coins = my_inventory.get_slot_of_item_by_def_id(&coins);
let get_their_coins = their_inventory.get_slot_of_item_by_def_id(&coins);
let coins_owned = COINS_ID.to_owned();
let get_my_coins = my_inventory.get_slot_of_item_by_def_id(&coins_owned);
let get_their_coins = their_inventory.get_slot_of_item_by_def_id(&coins_owned);
let (mut my_offered_coin_amount, mut their_offered_coin_amount) = (0, 0);
let my_offered_items_value =
@ -557,18 +563,20 @@ impl Bot {
.into_iter()
.fold(0, |acc: i32, (slot_id, quantity)| {
if let Some(item) = my_inventory.get(*slot_id) {
let item_id = item.persistence_item_id();
let item_id = item.item_definition_id();
let item_value = if item_id == COINS {
let item_value = if item_id == COINS_ID {
my_offered_coin_amount = *quantity as i32;
1
} else {
self.sell_prices
.simple
.get(&item_id)
.map(|int| *int as i32)
.unwrap_or_else(|| {
self.buy_prices
.simple
.get(&item_id)
.map(|int| 0 - *int as i32)
.unwrap_or(i32::MIN)
@ -585,18 +593,20 @@ impl Bot {
.into_iter()
.fold(0, |acc: i32, (slot_id, quantity)| {
if let Some(item) = their_inventory.get(*slot_id) {
let item_id = item.persistence_item_id();
let item_id = item.item_definition_id();
let item_value = if item_id == COINS {
let item_value = if item_id == COINS_ID {
their_offered_coin_amount = *quantity as i32;
1
} else {
self.buy_prices
.simple
.get(&item_id)
.map(|int| *int as i32)
.unwrap_or_else(|| {
self.sell_prices
.simple
.get(&item_id)
.map(|int| 0 - *int as i32)
.unwrap_or(0)
@ -613,13 +623,13 @@ impl Bot {
for (slot_id, amount) in my_offer {
let item = my_inventory.get(*slot_id).ok_or("Failed to get item")?;
let item_id = item.persistence_item_id();
let item_id = item.item_definition_id();
if item_id == COINS {
if item_id == COINS_ID {
continue;
}
if !self.sell_prices.contains_key(&item_id) {
if !self.sell_prices.simple.contains_key(&item_id) {
my_item_to_remove = Some((slot_id, amount));
}
}
@ -628,13 +638,13 @@ impl Bot {
for (slot_id, amount) in their_offer {
let item = their_inventory.get(*slot_id).ok_or("Failed to get item")?;
let item_id = item.persistence_item_id();
let item_id = item.item_definition_id();
if item_id == COINS {
if item_id == COINS_ID {
continue;
}
if !self.buy_prices.contains_key(&item_id) {
if !self.buy_prices.simple.contains_key(&item_id) {
their_item_to_remove = Some((slot_id, amount));
}
}
@ -741,8 +751,8 @@ impl Bot {
.to_string();
let mut found = false;
for (item_id, price) in &self.buy_prices {
let item_name = self.get_item_name(item_id);
for (item_id, price) in &self.buy_prices.simple {
let item_name = self.get_item_name(item_id.as_ref());
if item_name.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_name} to {player_name}");
@ -756,16 +766,60 @@ impl Bot {
);
found = true;
} else if item_id.contains(&search_term) {
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
log::info!("Sending price info on {short_id} to {player_name}");
continue;
}
if let Some(item_id_string) = item_id.as_ref().itemdef_id() {
if item_id_string.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_id_string} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Buying {short_id} for {price} coins."),
format!("Buying {item_id_string} for {price} coins."),
],
);
found = true;
}
}
}
for modular_item_listing in &self.buy_prices.modular {
let material = ItemDefinitionId::Simple(Cow::Borrowed(
modular_item_listing
.material
.asset_identifier()
.ok_or(format!(
"{:?} is not a valid material for modular crafted items",
modular_item_listing.material
))?,
));
let primary = modular_item_listing.primary.as_ref();
let secondary = ItemDefinitionId::Compound {
// This unwrap is safe because the ItemDefinitionId is always Simple.
simple_base: primary.itemdef_id().unwrap(),
components: vec![material],
};
let item_id = ItemDefinitionId::Modular {
pseudo_base: "veloren.core.pseudo_items.modular.tool",
components: vec![secondary],
};
let item_name = self.get_item_name(item_id);
if item_name.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_name} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!(
"Buying {item_name} for {} coins.",
modular_item_listing.price
),
],
);
@ -773,8 +827,8 @@ impl Bot {
}
}
for (item_id, price) in &self.sell_prices {
let item_name = self.get_item_name(item_id);
for (item_id, price) in &self.sell_prices.simple {
let item_name = self.get_item_name(item_id.as_ref());
if item_name.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_name} to {player_name}");
@ -788,16 +842,60 @@ impl Bot {
);
found = true;
} else if item_id.contains(&search_term) {
let short_id = item_id.splitn(3, '.').last().unwrap_or_default();
log::info!("Sending price info on {short_id} to {player_name}");
continue;
}
if let Some(item_id_string) = item_id.as_ref().itemdef_id() {
if item_id_string.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_id_string} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!("Selling {short_id} for {price} coins."),
format!("Selling {item_id_string} for {price} coins."),
],
);
found = true;
}
}
}
for modular_item_listing in &self.sell_prices.modular {
let material = ItemDefinitionId::Simple(Cow::Borrowed(
modular_item_listing
.material
.asset_identifier()
.ok_or(format!(
"{:?} is not a valid material for modular crafted items",
modular_item_listing.material
))?,
));
let primary = modular_item_listing.primary.as_ref();
let secondary = ItemDefinitionId::Compound {
// This unwrap is safe because the ItemDefinitionId is always Simple.
simple_base: primary.itemdef_id().unwrap(),
components: vec![material],
};
let item_id = ItemDefinitionId::Modular {
pseudo_base: "veloren.core.pseudo_items.modular.tool",
components: vec![secondary],
};
let item_name = self.get_item_name(item_id);
if item_name.to_lowercase().contains(&search_term) {
log::info!("Sending price info on {item_name} to {player_name}");
self.client.send_command(
"tell".to_string(),
vec![
player_name.clone(),
format!(
"Selling {item_name} for {} coins.",
modular_item_listing.price
),
],
);
@ -866,12 +964,9 @@ impl Bot {
}
/// Gets the name of an item from its id.
fn get_item_name(&self, item_id: &str) -> String {
let item = Item::new_from_item_definition_id(
ItemDefinitionId::Simple(Cow::Borrowed(item_id)),
&self.ability_map,
&self.material_manifest,
)
fn get_item_name(&self, item_id: ItemDefinitionId) -> String {
let item =
Item::new_from_item_definition_id(item_id, &self.ability_map, &self.material_manifest)
.unwrap();
let (item_name_i18n_id, _) = item.i18n(&self.item_i18n);

View File

@ -2,24 +2,165 @@
mod bot;
use std::{env::var, fs::read_to_string};
use std::{borrow::Cow, env::var, fs::read_to_string, str::FromStr};
use bot::Bot;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use serde::{de::Visitor, Deserialize};
use veloren_common::comp::item::{ItemDefinitionId, ItemDefinitionIdOwned, Material};
#[derive(Serialize, Deserialize)]
pub struct PriceList {
pub simple: HashMap<ItemDefinitionIdOwned, u32>,
pub modular: Vec<ModularItemPrice>,
}
impl<'de> Deserialize<'de> for PriceList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(PriceListVisitor)
}
}
pub struct PriceListVisitor;
impl<'de> Visitor<'de> for PriceListVisitor {
type Value = PriceList;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map with simple and/or modular keys")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut simple = None;
let mut modular = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"simple" => {
let simple_prices_with_item_string =
map.next_value::<HashMap<String, u32>>()?;
let simple_prices_with_item_id = simple_prices_with_item_string
.into_iter()
.map(|(mut key, value)| {
key.insert_str(0, "common.items.");
(ItemDefinitionIdOwned::Simple(key), value)
})
.collect();
simple = Some(simple_prices_with_item_id);
}
"modular" => {
modular = Some(map.next_value()?);
}
_ => {
return Err(serde::de::Error::unknown_field(
&key,
&["simple", "modular"],
));
}
}
}
Ok(PriceList {
simple: simple.ok_or_else(|| serde::de::Error::missing_field("simple"))?,
modular: modular.ok_or_else(|| serde::de::Error::missing_field("modular"))?,
})
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ModularItemPrice {
pub material: Material,
pub primary: ItemDefinitionIdOwned,
pub secondary: ItemDefinitionIdOwned,
pub price: u32,
}
impl<'de> Deserialize<'de> for ModularItemPrice {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(ModularPriceVisitor)
}
}
struct ModularPriceVisitor;
impl<'de> Visitor<'de> for ModularPriceVisitor {
type Value = ModularItemPrice;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map with material, primary, secondary and price keys")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut material = None;
let mut primary = None;
let mut secondary = None;
let mut price = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"material" => {
material = Some(map.next_value()?);
}
"primary" => {
let mut primary_string = map.next_value::<String>()?;
primary_string.insert_str(0, "common.items.modular.weapon.primary.");
primary = Some(ItemDefinitionIdOwned::Simple(primary_string));
}
"secondary" => {
let mut secondary_string = map.next_value::<String>()?;
secondary_string.insert_str(0, "common.items.modular.weapon.secondary.");
secondary = Some(ItemDefinitionIdOwned::Simple(secondary_string));
}
"price" => {
price = Some(map.next_value()?);
}
_ => {
return Err(serde::de::Error::unknown_field(
&key,
&["material", "primary", "secondary", "price"],
));
}
}
}
Ok(ModularItemPrice {
material: material.ok_or_else(|| serde::de::Error::missing_field("material"))?,
primary: primary.ok_or_else(|| serde::de::Error::missing_field("primary"))?,
secondary: secondary.ok_or_else(|| serde::de::Error::missing_field("secondary"))?,
price: price.ok_or_else(|| serde::de::Error::missing_field("price"))?,
})
}
}
#[derive(Deserialize)]
struct Config {
pub game_server: Option<String>,
pub auth_server: Option<String>,
pub position: Option<[f32; 3]>,
pub orientation: Option<f32>,
pub announcement: Option<String>,
pub buy_prices: HashMap<String, u32>,
pub sell_prices: HashMap<String, u32>,
pub buy_prices: PriceList,
pub sell_prices: PriceList,
}
#[derive(Serialize, Deserialize)]
#[derive(Deserialize)]
pub struct Secrets {
pub username: String,
pub password: String,
@ -50,24 +191,6 @@ fn main() {
let auth_server = config
.auth_server
.unwrap_or("https://auth.veloren.net".to_string());
let buy_prices_with_full_id = config
.buy_prices
.into_iter()
.map(|(mut item_id, price)| {
item_id.insert_str(0, "common.items.");
(item_id, price)
})
.collect();
let sell_prices_with_full_id = config
.sell_prices
.into_iter()
.map(|(mut item_id, price)| {
item_id.insert_str(0, "common.items.");
(item_id, price)
})
.collect();
let mut bot = Bot::new(
game_server,
&auth_server,
@ -75,8 +198,8 @@ fn main() {
&secrets.password,
&secrets.character,
secrets.admins,
buy_prices_with_full_id,
sell_prices_with_full_id,
config.buy_prices,
config.sell_prices,
config.position,
config.orientation,
config.announcement,