Merge branch 'Character-editting' into 'master'

Character editing

See merge request veloren/veloren!3059
This commit is contained in:
Ben Wallis 2021-12-20 15:24:43 +00:00
commit 9a4e6b81d3
19 changed files with 583 additions and 239 deletions

BIN
assets/voxygen/element/ui/char_select/icons/pen.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/char_select/icons/pen_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/ui/char_select/icons/pen_press.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -37,6 +37,7 @@
"common.automatic": "Auto",
"common.random": "Random",
"common.empty": "Empty",
"common.confirm": "Confirm",
// Settings Window title
"common.interface_settings": "Interface Settings",

View File

@ -108,6 +108,7 @@ pub enum Event {
SetViewDistance(u32),
Outcome(Outcome),
CharacterCreated(CharacterId),
CharacterEdited(CharacterId),
CharacterError(String),
}
@ -738,6 +739,7 @@ impl Client {
let stream = match msg {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
| ClientGeneral::Character(_)
| ClientGeneral::Spectate => &mut self.character_screen_stream,
@ -842,6 +844,11 @@ impl Client {
});
}
pub fn edit_character(&mut self, alias: String, id: CharacterId, body: comp::Body) {
self.character_list.loading = true;
self.send_msg(ClientGeneral::EditCharacter { alias, id, body });
}
/// Character deletion
pub fn delete_character(&mut self, character_id: CharacterId) {
self.character_list.loading = true;
@ -2037,6 +2044,9 @@ impl Client {
ServerGeneral::CharacterCreated(character_id) => {
events.push(Event::CharacterCreated(character_id));
},
ServerGeneral::CharacterEdited(character_id) => {
events.push(Event::CharacterEdited(character_id));
},
ServerGeneral::CharacterSuccess => {
debug!("client is now in ingame state on server");
if let Some(vd) = self.view_distance {

View File

@ -55,6 +55,11 @@ pub enum ClientGeneral {
body: comp::Body,
},
DeleteCharacter(CharacterId),
EditCharacter {
id: CharacterId,
alias: String,
body: comp::Body,
},
Character(CharacterId),
Spectate,
//Only in game
@ -105,6 +110,7 @@ impl ClientMsg {
&& match g {
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_) => {
c_type != ClientType::ChatOnly && presence.is_none()
},

View File

@ -133,6 +133,7 @@ pub enum ServerGeneral {
CharacterActionError(String),
/// A new character was created
CharacterCreated(character::CharacterId),
CharacterEdited(character::CharacterId),
CharacterSuccess,
//Ingame related
GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
@ -275,6 +276,7 @@ impl ServerMsg {
ServerGeneral::CharacterDataLoadError(_)
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterCreated(_) => {
c_type != ClientType::ChatOnly && presence.is_none()
},

View File

@ -1,6 +1,7 @@
use crate::persistence::character_updater::CharacterUpdater;
use common::comp::{
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
use common::{
character::CharacterId,
comp::{inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats},
};
use specs::{Entity, WriteExpect};
@ -73,6 +74,22 @@ pub fn create_character(
Ok(())
}
pub fn edit_character(
entity: Entity,
player_uuid: String,
id: CharacterId,
character_alias: String,
body: Body,
character_updater: &mut WriteExpect<'_, CharacterUpdater>,
) -> Result<(), CreationError> {
if !matches!(body, Body::Humanoid(_)) {
return Err(CreationError::InvalidBody);
}
character_updater.edit_character(entity, player_uuid, id, character_alias, (body,));
Ok(())
}
// Error handling
impl core::fmt::Display for CreationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {

View File

@ -94,6 +94,7 @@ impl Client {
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterSuccess => {
self.character_screen_stream.lock().unwrap().send(g)
},
@ -165,6 +166,7 @@ impl Client {
| ServerGeneral::CharacterListUpdate(_)
| ServerGeneral::CharacterActionError(_)
| ServerGeneral::CharacterCreated(_)
| ServerGeneral::CharacterEdited(_)
| ServerGeneral::CharacterSuccess => {
PreparedMsg::new(1, &g, &self.character_screen_stream_params)
},

View File

@ -813,6 +813,22 @@ impl Server {
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterEdit(result) => match result {
Ok((character_id, list)) => {
self.notify_client(
query_result.entity,
ServerGeneral::CharacterListUpdate(list),
);
self.notify_client(
query_result.entity,
ServerGeneral::CharacterEdited(character_id),
);
},
Err(error) => self.notify_client(
query_result.entity,
ServerGeneral::CharacterActionError(error.to_string()),
),
},
CharacterLoaderResponseKind::CharacterData(result) => {
let message = match *result {
Ok(character_data) => ServerEvent::UpdateCharacterData {

View File

@ -22,7 +22,7 @@ use crate::{
character_loader::{CharacterCreationResult, CharacterDataResult, CharacterListResult},
character_updater::PetPersistenceData,
error::PersistenceError::DatabaseError,
PersistedComponents,
EditableComponents, PersistedComponents,
},
};
use common::character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER};
@ -277,8 +277,8 @@ pub fn load_character_list(player_uuid_: &str, connection: &Connection) -> Chara
let mut stmt = connection.prepare_cached(
"
SELECT character_id,
alias
FROM character
alias
FROM character
WHERE player_uuid = ?1
ORDER BY character_id",
)?;
@ -497,6 +497,58 @@ pub fn create_character(
load_character_list(uuid, transactionn).map(|list| (character_id, list))
}
pub fn edit_character(
editable_components: EditableComponents,
transaction: &mut Transaction,
character_id: CharacterId,
uuid: &str,
character_alias: &str,
) -> CharacterCreationResult {
let (body,) = editable_components;
let mut char_list = load_character_list(uuid, transaction);
if let Ok(char_list) = &mut char_list {
if let Some(char) = char_list
.iter_mut()
.find(|c| c.character.id == Some(character_id))
{
if let (crate::comp::Body::Humanoid(new), crate::comp::Body::Humanoid(old)) =
(body, char.body)
{
if new.species != old.species || new.body_type != old.body_type {
warn!(
"Character edit rejected due to failed validation - Character ID: {} \
Alias: {}",
character_id, character_alias
);
return Err(PersistenceError::CharacterDataError);
} else {
char.body = body;
}
}
}
}
let mut stmt = transaction
.prepare_cached("UPDATE body SET variant = ?1, body_data = ?2 WHERE body_id = ?3")?;
let (body_variant, body_data) = convert_body_to_database_json(&body)?;
stmt.execute(&[
&body_variant.to_string(),
&body_data,
&character_id as &dyn ToSql,
])?;
drop(stmt);
let mut stmt =
transaction.prepare_cached("UPDATE character SET alias = ?1 WHERE character_id = ?2")?;
stmt.execute(&[&character_alias, &character_id as &dyn ToSql])?;
drop(stmt);
char_list.map(|list| (character_id, list))
}
/// Delete a character. Returns the updated character list.
pub fn delete_character(
requesting_player_uuid: &str,
@ -846,7 +898,7 @@ fn delete_pets(
#[rustfmt::skip]
let mut stmt = transaction.prepare_cached("
DELETE
DELETE
FROM body
WHERE body_id IN rarray(?1)"
)?;
@ -1009,7 +1061,7 @@ pub fn update(
let mut stmt = transaction.prepare_cached(
"
REPLACE
REPLACE
INTO skill (entity_id,
skill,
level)

View File

@ -12,6 +12,7 @@ use tracing::error;
pub(crate) type CharacterListResult = Result<Vec<CharacterItem>, PersistenceError>;
pub(crate) type CharacterCreationResult =
Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
pub(crate) type CharacterEditResult = Result<(CharacterId, Vec<CharacterItem>), PersistenceError>;
pub(crate) type CharacterDataResult = Result<PersistedComponents, PersistenceError>;
type CharacterLoaderRequest = (specs::Entity, CharacterLoaderRequestKind);
@ -33,6 +34,7 @@ pub enum CharacterLoaderResponseKind {
CharacterList(CharacterListResult),
CharacterData(Box<CharacterDataResult>),
CharacterCreation(CharacterCreationResult),
CharacterEdit(CharacterEditResult),
}
/// Common message format dispatched in response to an update request

View File

@ -4,10 +4,11 @@ use common::character::CharacterId;
use crate::persistence::{
character_loader::{CharacterLoaderResponse, CharacterLoaderResponseKind},
error::PersistenceError,
establish_connection, ConnectionMode, DatabaseSettings, PersistedComponents, VelorenConnection,
establish_connection, ConnectionMode, DatabaseSettings, EditableComponents,
PersistedComponents, VelorenConnection,
};
use crossbeam_channel::TryIter;
use rusqlite::DropBehavior;
use rusqlite::{DropBehavior, Transaction};
use specs::Entity;
use std::{
collections::HashMap,
@ -36,6 +37,13 @@ pub enum CharacterUpdaterEvent {
character_alias: String,
persisted_components: PersistedComponents,
},
EditCharacter {
entity: Entity,
player_uuid: String,
character_id: CharacterId,
character_alias: String,
editable_components: EditableComponents,
},
DeleteCharacter {
entity: Entity,
requesting_player_uuid: String,
@ -124,6 +132,37 @@ impl CharacterUpdater {
),
}
},
CharacterUpdaterEvent::EditCharacter {
entity,
character_id,
character_alias,
player_uuid,
editable_components,
} => {
match execute_character_edit(
entity,
character_id,
character_alias,
&player_uuid,
editable_components,
&mut conn,
) {
Ok(response) => {
if let Err(e) = response_tx.send(response) {
error!(?e, "Could not send character edit response");
} else {
debug!(
"Processed character edit for player {}",
player_uuid
);
}
},
Err(e) => error!(
"Error editing character for player {}, error: {:?}",
player_uuid, e
),
}
},
CharacterUpdaterEvent::DeleteCharacter {
entity,
requesting_player_uuid,
@ -231,6 +270,30 @@ impl CharacterUpdater {
}
}
pub fn edit_character(
&mut self,
entity: Entity,
requesting_player_uuid: String,
character_id: CharacterId,
alias: String,
editable_components: EditableComponents,
) {
if let Err(e) =
self.update_tx
.as_ref()
.unwrap()
.send(CharacterUpdaterEvent::EditCharacter {
entity,
player_uuid: requesting_player_uuid,
character_id,
character_alias: alias,
editable_components,
})
{
error!(?e, "Could not send character edit request");
}
}
pub fn delete_character(
&mut self,
entity: Entity,
@ -345,22 +408,33 @@ fn execute_character_create(
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let response = CharacterLoaderResponse {
entity,
result: CharacterLoaderResponseKind::CharacterCreation(super::character::create_character(
let result =
CharacterLoaderResponseKind::CharacterCreation(super::character::create_character(
requesting_player_uuid,
&alias,
persisted_components,
&mut transaction,
)),
};
));
check_response(entity, transaction, result)
}
if !response.is_err() {
transaction.commit()?;
};
Ok(response)
fn execute_character_edit(
entity: Entity,
character_id: CharacterId,
alias: String,
requesting_player_uuid: &str,
editable_components: EditableComponents,
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let result = CharacterLoaderResponseKind::CharacterEdit(super::character::edit_character(
editable_components,
&mut transaction,
character_id,
requesting_player_uuid,
&alias,
));
check_response(entity, transaction, result)
}
fn execute_character_delete(
@ -370,15 +444,20 @@ fn execute_character_delete(
connection: &mut VelorenConnection,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let mut transaction = connection.connection.transaction()?;
let result = CharacterLoaderResponseKind::CharacterList(super::character::delete_character(
requesting_player_uuid,
character_id,
&mut transaction,
));
check_response(entity, transaction, result)
}
let response = CharacterLoaderResponse {
entity,
result: CharacterLoaderResponseKind::CharacterList(super::character::delete_character(
requesting_player_uuid,
character_id,
&mut transaction,
)),
};
fn check_response(
entity: Entity,
transaction: Transaction,
result: CharacterLoaderResponseKind,
) -> Result<CharacterLoaderResponse, PersistenceError> {
let response = CharacterLoaderResponse { entity, result };
if !response.is_err() {
transaction.commit()?;

View File

@ -31,6 +31,8 @@ pub type PersistedComponents = (
Vec<PetPersistenceData>,
);
pub type EditableComponents = (comp::Body,);
// See: https://docs.rs/refinery/0.5.0/refinery/macro.embed_migrations.html
// This macro is called at build-time, and produces the necessary migration info
// for the `run_migrations` call below.

View File

@ -145,6 +145,28 @@ impl Sys {
}
}
},
ClientGeneral::EditCharacter { id, alias, body } => {
if let Err(error) = alias_validator.validate(&alias) {
debug!(?error, ?alias, "denied alias as it contained a banned word");
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
} else if let Some(player) = players.get(entity) {
if let Err(error) = character_creator::edit_character(
entity,
player.uuid().to_string(),
id,
alias,
body,
character_updater,
) {
debug!(
?error,
?body,
"Denied editing character because of invalid input."
);
client.send(ServerGeneral::CharacterActionError(error.to_string()))?;
}
}
},
ClientGeneral::DeleteCharacter(character_id) => {
if let Some(player) = players.get(entity) {
character_updater.delete_character(

View File

@ -283,6 +283,7 @@ impl Sys {
},
ClientGeneral::RequestCharacterList
| ClientGeneral::CreateCharacter { .. }
| ClientGeneral::EditCharacter { .. }
| ClientGeneral::DeleteCharacter(_)
| ClientGeneral::Character(_)
| ClientGeneral::Spectate

View File

@ -119,6 +119,15 @@ impl PlayState for CharSelectionState {
.borrow_mut()
.create_character(alias, mainhand, offhand, body);
},
ui::Event::EditCharacter {
alias,
character_id,
body,
} => {
self.client
.borrow_mut()
.edit_character(alias, character_id, body);
},
ui::Event::DeleteCharacter(character_id) => {
self.client.borrow_mut().delete_character(character_id);
},

View File

@ -76,6 +76,10 @@ image_ids_ice! {
delete_button_hover: "voxygen.element.ui.char_select.icons.bin_hover",
delete_button_press: "voxygen.element.ui.char_select.icons.bin_press",
edit_button: "voxygen.element.ui.char_select.icons.pen",
edit_button_hover: "voxygen.element.ui.char_select.icons.pen_hover",
edit_button_press: "voxygen.element.ui.char_select.icons.pen_press",
name_input: "voxygen.element.ui.generic.textbox",
// Tool Icons
@ -129,6 +133,11 @@ pub enum Event {
offhand: Option<String>,
body: comp::Body,
},
EditCharacter {
alias: String,
character_id: CharacterId,
body: comp::Body,
},
DeleteCharacter(CharacterId),
ClearCharacterListError,
SelectCharacter(Option<CharacterId>),
@ -146,7 +155,7 @@ enum Mode {
yes_button: button::State,
no_button: button::State,
},
Create {
CreateOrEdit {
name: String,
body: humanoid::Body,
inventory: Box<comp::inventory::Inventory>,
@ -163,6 +172,7 @@ enum Mode {
create_button: button::State,
rand_character_button: button::State,
rand_name_button: button::State,
character_id: Option<CharacterId>,
},
}
@ -194,7 +204,7 @@ impl Mode {
let inventory = Box::new(Inventory::new_with_loadout(loadout));
Self::Create {
Self::CreateOrEdit {
name,
body: humanoid::Body::random(),
inventory,
@ -210,6 +220,33 @@ impl Mode {
create_button: Default::default(),
rand_character_button: Default::default(),
rand_name_button: Default::default(),
character_id: None,
}
}
pub fn edit(
name: String,
character_id: CharacterId,
body: humanoid::Body,
inventory: &Inventory,
) -> Self {
Self::CreateOrEdit {
name,
body,
inventory: Box::new(inventory.clone()),
mainhand: None,
offhand: None,
body_type_buttons: Default::default(),
species_buttons: Default::default(),
tool_buttons: Default::default(),
sliders: Default::default(),
scroll: Default::default(),
name_input: Default::default(),
back_button: Default::default(),
create_button: Default::default(),
rand_character_button: Default::default(),
rand_name_button: Default::default(),
character_id: Some(character_id),
}
}
}
@ -219,6 +256,7 @@ enum InfoContent {
Deletion(usize),
LoadingCharacters,
CreatingCharacter,
EditingCharacter,
DeletingCharacter,
CharacterError(String),
}
@ -247,6 +285,8 @@ enum Message {
EnterWorld,
Select(CharacterId),
Delete(usize),
Edit(usize),
ConfirmEdit(CharacterId),
NewCharacter,
CreateCharacter,
Name(String),
@ -394,6 +434,7 @@ impl Controls {
info_content,
Some(InfoContent::LoadingCharacters)
| Some(InfoContent::CreatingCharacter)
| Some(InfoContent::EditingCharacter)
| Some(InfoContent::DeletingCharacter)
) && !client.character_list().loading
{
@ -421,12 +462,13 @@ impl Controls {
let characters = &client.character_list().characters;
let num = characters.len();
// Ensure we have enough button states
character_buttons.resize_with(num * 2, Default::default);
const CHAR_BUTTONS: usize = 3;
character_buttons.resize_with(num * CHAR_BUTTONS, Default::default);
// Character Selection List
let mut characters = characters
.iter()
.zip(character_buttons.chunks_exact_mut(2))
.zip(character_buttons.chunks_exact_mut(CHAR_BUTTONS))
.filter_map(|(character, buttons)| {
let mut buttons = buttons.iter_mut();
// TODO: eliminate option in character id?
@ -434,13 +476,24 @@ impl Controls {
(
id,
character,
(buttons.next().unwrap(), buttons.next().unwrap()),
(
buttons.next().unwrap(),
buttons.next().unwrap(),
buttons.next().unwrap(),
),
)
})
})
.enumerate()
.map(
|(i, (character_id, character, (select_button, delete_button)))| {
|(
i,
(
character_id,
character,
(select_button, edit_button, delete_button),
),
)| {
let select_col = if Some(i) == selected {
(255, 208, 69)
} else {
@ -448,17 +501,33 @@ impl Controls {
};
Overlay::new(
Container::new(
// Delete button
Button::new(
delete_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.delete_button)
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
)
.on_press(Message::Delete(i)),
Row::with_children(vec![
// Edit button
Button::new(
edit_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.edit_button)
.hover_image(imgs.edit_button_hover)
.press_image(imgs.edit_button_press),
)
.on_press(Message::Edit(i))
.into(),
// Delete button
Button::new(
delete_button,
Space::new(Length::Units(16), Length::Units(16)),
)
.style(
style::button::Style::new(imgs.delete_button)
.hover_image(imgs.delete_button_hover)
.press_image(imgs.delete_button_press),
)
.on_press(Message::Delete(i))
.into(),
])
.spacing(5),
)
.padding(4),
// Select Button
@ -668,6 +737,11 @@ impl Controls {
.size(fonts.cyri.scale(24))
.into()
},
InfoContent::EditingCharacter => {
Text::new(i18n.get("char_selection.editing_character"))
.size(fonts.cyri.scale(24))
.into()
},
InfoContent::DeletingCharacter => {
Text::new(i18n.get("char_selection.deleting_character"))
.size(fonts.cyri.scale(24))
@ -716,7 +790,7 @@ impl Controls {
content.into()
}
},
Mode::Create {
Mode::CreateOrEdit {
name,
body,
inventory: _,
@ -732,6 +806,7 @@ impl Controls {
ref mut create_button,
ref mut rand_character_button,
ref mut rand_name_button,
character_id,
} => {
let unselected_style = style::button::Style::new(imgs.icon_border)
.hover_image(imgs.icon_border_mo)
@ -763,190 +838,192 @@ impl Controls {
})
};
let (body_m_ico, body_f_ico) = match body.species {
humanoid::Species::Human => (imgs.human_m, imgs.human_f),
humanoid::Species::Orc => (imgs.orc_m, imgs.orc_f),
humanoid::Species::Dwarf => (imgs.dwarf_m, imgs.dwarf_f),
humanoid::Species::Elf => (imgs.elf_m, imgs.elf_f),
humanoid::Species::Undead => (imgs.undead_m, imgs.undead_f),
humanoid::Species::Danari => (imgs.danari_m, imgs.danari_f),
};
let [ref mut body_m_button, ref mut body_f_button] = body_type_buttons;
let body_type = Row::with_children(vec![
icon_button(
body_m_button,
matches!(body.body_type, humanoid::BodyType::Male),
Message::BodyType(humanoid::BodyType::Male),
body_m_ico,
)
.into(),
icon_button(
body_f_button,
matches!(body.body_type, humanoid::BodyType::Female),
Message::BodyType(humanoid::BodyType::Female),
body_f_ico,
)
.into(),
])
.spacing(1);
let (human_icon, orc_icon, dwarf_icon, elf_icon, undead_icon, danari_icon) =
match body.body_type {
humanoid::BodyType::Male => (
self.imgs.human_m,
self.imgs.orc_m,
self.imgs.dwarf_m,
self.imgs.elf_m,
self.imgs.undead_m,
self.imgs.danari_m,
),
humanoid::BodyType::Female => (
self.imgs.human_f,
self.imgs.orc_f,
self.imgs.dwarf_f,
self.imgs.elf_f,
self.imgs.undead_f,
self.imgs.danari_f,
),
};
// TODO: tooltips
let [
ref mut human_button,
ref mut orc_button,
ref mut dwarf_button,
ref mut elf_button,
ref mut undead_button,
ref mut danari_button,
] = species_buttons;
let species = Column::with_children(vec![
Row::with_children(vec![
icon_button_tooltip(
human_button,
matches!(body.species, humanoid::Species::Human),
Message::Species(humanoid::Species::Human),
human_icon,
"common.species.human",
let (tool, species, body_type) = if character_id.is_some() {
(Column::new(), Column::new(), Row::new())
} else {
let (body_m_ico, body_f_ico) = match body.species {
humanoid::Species::Human => (imgs.human_m, imgs.human_f),
humanoid::Species::Orc => (imgs.orc_m, imgs.orc_f),
humanoid::Species::Dwarf => (imgs.dwarf_m, imgs.dwarf_f),
humanoid::Species::Elf => (imgs.elf_m, imgs.elf_f),
humanoid::Species::Undead => (imgs.undead_m, imgs.undead_f),
humanoid::Species::Danari => (imgs.danari_m, imgs.danari_f),
};
let [ref mut body_m_button, ref mut body_f_button] = body_type_buttons;
let body_type = Row::with_children(vec![
icon_button(
body_m_button,
matches!(body.body_type, humanoid::BodyType::Male),
Message::BodyType(humanoid::BodyType::Male),
body_m_ico,
)
.into(),
icon_button_tooltip(
orc_button,
matches!(body.species, humanoid::Species::Orc),
Message::Species(humanoid::Species::Orc),
orc_icon,
"common.species.orc",
)
.into(),
icon_button_tooltip(
dwarf_button,
matches!(body.species, humanoid::Species::Dwarf),
Message::Species(humanoid::Species::Dwarf),
dwarf_icon,
"common.species.dwarf",
icon_button(
body_f_button,
matches!(body.body_type, humanoid::BodyType::Female),
Message::BodyType(humanoid::BodyType::Female),
body_f_ico,
)
.into(),
])
.spacing(1)
.into(),
Row::with_children(vec![
icon_button_tooltip(
elf_button,
matches!(body.species, humanoid::Species::Elf),
Message::Species(humanoid::Species::Elf),
elf_icon,
"common.species.elf",
)
.spacing(1);
let (human_icon, orc_icon, dwarf_icon, elf_icon, undead_icon, danari_icon) =
match body.body_type {
humanoid::BodyType::Male => (
self.imgs.human_m,
self.imgs.orc_m,
self.imgs.dwarf_m,
self.imgs.elf_m,
self.imgs.undead_m,
self.imgs.danari_m,
),
humanoid::BodyType::Female => (
self.imgs.human_f,
self.imgs.orc_f,
self.imgs.dwarf_f,
self.imgs.elf_f,
self.imgs.undead_f,
self.imgs.danari_f,
),
};
let [
ref mut human_button,
ref mut orc_button,
ref mut dwarf_button,
ref mut elf_button,
ref mut undead_button,
ref mut danari_button,
] = species_buttons;
let species = Column::with_children(vec![
Row::with_children(vec![
icon_button_tooltip(
human_button,
matches!(body.species, humanoid::Species::Human),
Message::Species(humanoid::Species::Human),
human_icon,
"common.species.human",
)
.into(),
icon_button_tooltip(
orc_button,
matches!(body.species, humanoid::Species::Orc),
Message::Species(humanoid::Species::Orc),
orc_icon,
"common.species.orc",
)
.into(),
icon_button_tooltip(
dwarf_button,
matches!(body.species, humanoid::Species::Dwarf),
Message::Species(humanoid::Species::Dwarf),
dwarf_icon,
"common.species.dwarf",
)
.into(),
])
.spacing(1)
.into(),
icon_button_tooltip(
undead_button,
matches!(body.species, humanoid::Species::Undead),
Message::Species(humanoid::Species::Undead),
undead_icon,
"common.species.undead",
)
.into(),
icon_button_tooltip(
danari_button,
matches!(body.species, humanoid::Species::Danari),
Message::Species(humanoid::Species::Danari),
danari_icon,
"common.species.danari",
)
Row::with_children(vec![
icon_button_tooltip(
elf_button,
matches!(body.species, humanoid::Species::Elf),
Message::Species(humanoid::Species::Elf),
elf_icon,
"common.species.elf",
)
.into(),
icon_button_tooltip(
undead_button,
matches!(body.species, humanoid::Species::Undead),
Message::Species(humanoid::Species::Undead),
undead_icon,
"common.species.undead",
)
.into(),
icon_button_tooltip(
danari_button,
matches!(body.species, humanoid::Species::Danari),
Message::Species(humanoid::Species::Danari),
danari_icon,
"common.species.danari",
)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1);
.spacing(1);
let [
ref mut sword_button,
ref mut swords_button,
ref mut axe_button,
ref mut hammer_button,
ref mut bow_button,
ref mut staff_button,
] = tool_buttons;
let tool = Column::with_children(vec![
Row::with_children(vec![
icon_button_tooltip(
sword_button,
*mainhand == Some(STARTER_SWORD),
Message::Tool((Some(STARTER_SWORD), None)),
imgs.sword,
"common.weapons.greatsword",
)
.into(),
icon_button_tooltip(
hammer_button,
*mainhand == Some(STARTER_HAMMER),
Message::Tool((Some(STARTER_HAMMER), None)),
imgs.hammer,
"common.weapons.hammer",
)
.into(),
icon_button_tooltip(
axe_button,
*mainhand == Some(STARTER_AXE),
Message::Tool((Some(STARTER_AXE), None)),
imgs.axe,
"common.weapons.axe",
)
.into(),
])
.spacing(1)
.into(),
Row::with_children(vec![
icon_button_tooltip(
swords_button,
*mainhand == Some(STARTER_SWORDS),
Message::Tool((Some(STARTER_SWORDS), Some(STARTER_SWORDS))),
imgs.swords,
"common.weapons.shortswords",
)
.into(),
icon_button_tooltip(
bow_button,
*mainhand == Some(STARTER_BOW),
Message::Tool((Some(STARTER_BOW), None)),
imgs.bow,
"common.weapons.bow",
)
.into(),
icon_button_tooltip(
staff_button,
*mainhand == Some(STARTER_STAFF),
Message::Tool((Some(STARTER_STAFF), None)),
imgs.staff,
"common.weapons.staff",
)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1);
let [
ref mut sword_button,
ref mut swords_button,
ref mut axe_button,
ref mut hammer_button,
ref mut bow_button,
ref mut staff_button,
] = tool_buttons;
let tool = Column::with_children(vec![
Row::with_children(vec![
icon_button_tooltip(
sword_button,
*mainhand == Some(STARTER_SWORD),
Message::Tool((Some(STARTER_SWORD), None)),
imgs.sword,
"common.weapons.greatsword",
)
.into(),
icon_button_tooltip(
hammer_button,
*mainhand == Some(STARTER_HAMMER),
Message::Tool((Some(STARTER_HAMMER), None)),
imgs.hammer,
"common.weapons.hammer",
)
.into(),
icon_button_tooltip(
axe_button,
*mainhand == Some(STARTER_AXE),
Message::Tool((Some(STARTER_AXE), None)),
imgs.axe,
"common.weapons.axe",
)
.into(),
])
.spacing(1)
.into(),
Row::with_children(vec![
icon_button_tooltip(
swords_button,
*mainhand == Some(STARTER_SWORDS),
Message::Tool((Some(STARTER_SWORDS), Some(STARTER_SWORDS))),
imgs.swords,
"common.weapons.shortswords",
)
.into(),
icon_button_tooltip(
bow_button,
*mainhand == Some(STARTER_BOW),
Message::Tool((Some(STARTER_BOW), None)),
imgs.bow,
"common.weapons.bow",
)
.into(),
icon_button_tooltip(
staff_button,
*mainhand == Some(STARTER_STAFF),
Message::Tool((Some(STARTER_STAFF), None)),
imgs.staff,
"common.weapons.staff",
)
.into(),
])
.spacing(1)
.into(),
])
.spacing(1);
(tool, species, body_type)
};
const SLIDER_TEXT_SIZE: u16 = 20;
const SLIDER_CURSOR_SIZE: (u16, u16) = (9, 21);
@ -1174,6 +1251,12 @@ impl Controls {
tooltip::text(i18n.get("common.rand_name"), tooltip_style)
});
let confirm_msg = if let Some(character_id) = character_id {
Message::ConfirmEdit(*character_id)
} else {
Message::CreateCharacter
};
let name_input = BackgroundContainer::new(
Image::new(imgs.name_input)
.height(Length::Units(40))
@ -1185,7 +1268,7 @@ impl Controls {
Message::Name,
)
.size(25)
.on_submit(Message::CreateCharacter),
.on_submit(confirm_msg.clone()),
)
.padding(Padding::new().horizontal(7).top(5));
@ -1204,10 +1287,14 @@ impl Controls {
let create = neat_button(
create_button,
i18n.get("common.create"),
i18n.get(if character_id.is_some() {
"common.confirm"
} else {
"common.create"
}),
FILL_FRAC_ONE,
button_style,
(!name.is_empty()).then_some(Message::CreateCharacter),
(!name.is_empty()).then_some(confirm_msg),
);
let create: Element<Message> = if name.is_empty() {
@ -1293,7 +1380,7 @@ impl Controls {
fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
match message {
Message::Back => {
if matches!(&self.mode, Mode::Create { .. }) {
if matches!(&self.mode, Mode::CreateOrEdit { .. }) {
self.mode = Mode::select(None);
}
},
@ -1316,13 +1403,29 @@ impl Controls {
*info_content = Some(InfoContent::Deletion(idx));
}
},
Message::Edit(idx) => {
if matches!(&self.mode, Mode::Select { .. }) {
if let Some(character) = characters.get(idx) {
if let comp::Body::Humanoid(body) = character.body {
if let Some(id) = character.character.id {
self.mode = Mode::edit(
character.character.alias.clone(),
id,
body,
&character.inventory,
);
}
}
}
}
},
Message::NewCharacter => {
if matches!(&self.mode, Mode::Select { .. }) {
self.mode = Mode::create(self.default_name.clone());
}
},
Message::CreateCharacter => {
if let Mode::Create {
if let Mode::CreateOrEdit {
name,
body,
mainhand,
@ -1339,25 +1442,35 @@ impl Controls {
self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
}
},
Message::ConfirmEdit(character_id) => {
if let Mode::CreateOrEdit { name, body, .. } = &self.mode {
events.push(Event::EditCharacter {
alias: name.clone(),
character_id,
body: comp::Body::Humanoid(*body),
});
self.mode = Mode::select(Some(InfoContent::EditingCharacter));
}
},
Message::Name(value) => {
if let Mode::Create { name, .. } = &mut self.mode {
if let Mode::CreateOrEdit { name, .. } = &mut self.mode {
*name = value.chars().take(MAX_NAME_LENGTH).collect();
}
},
Message::BodyType(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.body_type = value;
body.validate();
}
},
Message::Species(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.species = value;
body.validate();
}
},
Message::Tool(value) => {
if let Mode::Create {
if let Mode::CreateOrEdit {
mainhand,
offhand,
inventory,
@ -1378,7 +1491,7 @@ impl Controls {
},
//Todo: Add species and body type to randomization.
Message::RandomizeCharacter => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
use rand::Rng;
let body_type = body.body_type;
let species = body.species;
@ -1394,7 +1507,7 @@ impl Controls {
},
Message::RandomizeName => {
if let Mode::Create { name, body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { name, body, .. } = &mut self.mode {
use common::npc;
*name = npc::get_npc_name(
npc::NpcKind::Humanoid,
@ -1429,43 +1542,43 @@ impl Controls {
events.push(Event::ClearCharacterListError);
},
Message::HairStyle(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.hair_style = value;
body.validate();
}
},
Message::HairColor(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.hair_color = value;
body.validate();
}
},
Message::Skin(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.skin = value;
body.validate();
}
},
Message::Eyes(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.eyes = value;
body.validate();
}
},
Message::EyeColor(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.eye_color = value;
body.validate();
}
},
Message::Accessory(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.accessory = value;
body.validate();
}
},
Message::Beard(value) => {
if let Mode::Create { body, .. } = &mut self.mode {
if let Mode::CreateOrEdit { body, .. } = &mut self.mode {
body.beard = value;
body.validate();
}
@ -1484,7 +1597,7 @@ impl Controls {
.selected
.and_then(|id| characters.iter().find(|i| i.character.id == Some(id)))
.map(|i| (i.body, &i.inventory)),
Mode::Create {
Mode::CreateOrEdit {
inventory, body, ..
} => Some((comp::Body::Humanoid(*body), inventory)),
}

View File

@ -301,6 +301,7 @@ impl SessionState {
},
client::Event::Outcome(outcome) => outcomes.push(outcome),
client::Event::CharacterCreated(_) => {},
client::Event::CharacterEdited(_) => {},
client::Event::CharacterError(error) => {
global_state.client_error = Some(error);
},