Merge branch 'isse/save-the-plants' into 'master'

Npcs can catch you stealing

See merge request veloren/veloren!4637
This commit is contained in:
Isse
2024-11-07 17:36:28 +00:00
24 changed files with 478 additions and 170 deletions

View File

@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Wild Legoom and Goblin mobs
- Bloodservants, Strigoi, and Scarlet Spectacles added to Halloween event
- IP Bans
- Npcs can catch you stealing.
### Changed

View File

@ -46,6 +46,7 @@ hud-deactivate = Deactivate
hud-collect = Collect
hud-pick_up = Pick up
hud-open = Open
hud-steal = Steal
hud-use = Use
hud-read = Read
hud-unlock-requires = Open with { $item }

View File

@ -298,6 +298,18 @@ npc-speech-witness_murder =
.a0 = Murderer!
.a1 = How could you do this?
.a2 = Aaargh!
npc-speech-witness_theft =
.a0 = That's not yours!
.a1 = Keep your hands to yourself.
.a2 = Don't touch that!
.a3 = Thief!
.a4 = Give that back.
.a5 = What do you think you're doing?
.a6 = Stop or I'll call for the guards.
npc-speech-witness_theft_owned =
.a0 = Hey! That's mine.
.a1 = Why are you touching my things?
.a2 = You're not welcome here if you take my stuff.
npc-speech-witness_enemy_murder =
.a0 = My Hero!
.a1 = Finally someone did it!

View File

@ -593,6 +593,12 @@ impl Block {
}
}
#[inline]
pub fn is_owned(&self) -> bool {
self.get_attr::<sprite::Owned>()
.is_ok_and(|sprite::Owned(b)| b)
}
/// The tool required to mine this block. For blocks that cannot be mined,
/// `None` is returned.
#[inline]
@ -657,6 +663,16 @@ impl Block {
#[inline]
pub fn kind(&self) -> BlockKind { self.kind }
/// If possible, copy the sprite/color data of the other block.
#[inline]
#[must_use]
pub fn with_data_of(mut self, other: Block) -> Self {
if self.is_filled() == other.is_filled() {
self = self.with_data(other.data());
}
self
}
/// If this block is a fluid, replace its sprite.
#[inline]
#[must_use]

View File

@ -114,23 +114,6 @@ sprites! {
Loom = 0x27,
DismantlingBench = 0x28,
RepairBench = 0x29,
// Containers
Chest = 0x30,
DungeonChest0 = 0x31,
DungeonChest1 = 0x32,
DungeonChest2 = 0x33,
DungeonChest3 = 0x34,
DungeonChest4 = 0x35,
DungeonChest5 = 0x36,
CoralChest = 0x37,
HaniwaUrn = 0x38,
TerracottaChest = 0x39,
SahaginChest = 0x3A,
CommonLockedChest = 0x3B,
ChestBuried = 0x3C,
Crate = 0x3D,
Barrel = 0x3E,
CrateBlock = 0x3F,
// Wall
HangingBasket = 0x50,
HangingSign = 0x51,
@ -155,7 +138,7 @@ sprites! {
Hearth = 0x72,
},
// Sprites representing plants that may grow over time (this does not include plant parts, like fruit).
Plant = 3 has Growth {
Plant = 3 has Growth, Owned {
// Cacti
BarrelCactus = 0x00,
RoundCactus = 0x01,
@ -251,7 +234,7 @@ sprites! {
},
// Solid resources
// TODO: Remove small variants, make deposit size be an attribute
Resources = 4 {
Resource = 4 has Owned {
// Gems and ores
// Woods and twigs
Twigs = 0x00,
@ -394,6 +377,24 @@ sprites! {
FireBowlGround = 4,
MesaLantern = 5,
},
Container = 9 has Ori, Owned {
Chest = 0x00,
DungeonChest0 = 0x01,
DungeonChest1 = 0x02,
DungeonChest2 = 0x03,
DungeonChest3 = 0x04,
DungeonChest4 = 0x05,
DungeonChest5 = 0x06,
CoralChest = 0x07,
HaniwaUrn = 0x08,
TerracottaChest = 0x09,
SahaginChest = 0x0A,
CommonLockedChest = 0x0B,
ChestBuried = 0x0C,
Crate = 0x0D,
Barrel = 0x0E,
CrateBlock = 0x0F,
},
}
attributes! {
@ -401,6 +402,7 @@ attributes! {
Growth { bits: 4, err: Infallible, from: |bits| Ok(Self(bits as u8)), into: |Growth(x)| x as u16 },
LightEnabled { bits: 1, err: Infallible, from: |bits| Ok(Self(bits == 1)), into: |LightEnabled(x)| x as u16 },
Damage { bits: 3, err: Infallible, from: |bits| Ok(Self(bits as u8)), into: |Damage(x)| x as u16 },
Owned { bits: 1, err: Infallible, from: |bits| Ok(Self(bits == 1)), into: |Owned(x)| x as u16 },
}
// The orientation of the sprite, 0..16
@ -419,6 +421,9 @@ impl Default for Growth {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct LightEnabled(pub bool);
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
pub struct Owned(pub bool);
impl Default for LightEnabled {
fn default() -> Self { Self(true) }
}

View File

@ -1,4 +1,8 @@
use common::{resources::TimeOfDay, rtsim::Actor};
use common::{
resources::TimeOfDay,
rtsim::{Actor, SiteId},
terrain::SpriteKind,
};
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::ops::Deref;
@ -21,7 +25,7 @@ pub use common::rtsim::ReportId;
#[derive(Clone, Serialize, Deserialize)]
pub struct Report {
pub kind: ReportKind,
pub at: TimeOfDay,
pub at_tod: TimeOfDay,
}
impl Report {
@ -37,13 +41,25 @@ impl Report {
DAYS * 5.0
}
},
// TODO: Could consider what was stolen here
ReportKind::Theft { .. } => DAYS * 1.5,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub enum ReportKind {
Death { actor: Actor, killer: Option<Actor> },
Death {
actor: Actor,
killer: Option<Actor>,
},
Theft {
thief: Actor,
/// Where the theft happened.
site: Option<SiteId>,
/// What was stolen.
sprite: SpriteKind,
},
}
#[derive(Clone, Default, Serialize, Deserialize)]
@ -56,8 +72,9 @@ impl Reports {
pub fn cleanup(&mut self, current_time: TimeOfDay) {
// Forget reports that are too old
self.reports
.retain(|_, report| (current_time.0 - report.at.0).max(0.0) < report.remember_for());
self.reports.retain(|_, report| {
(current_time.0 - report.at_tod.0).max(0.0) < report.remember_for()
});
// TODO: Limit global number of reports
}
}

View File

@ -2,7 +2,8 @@ use crate::{RtState, Rule};
use common::{
mounting::VolumePos,
resources::{Time, TimeOfDay},
rtsim::{Actor, NpcId},
rtsim::{Actor, NpcId, SiteId},
terrain::SpriteKind,
};
use vek::*;
use world::{IndexRef, World};
@ -38,6 +39,16 @@ pub struct OnDeath {
}
impl Event for OnDeath {}
#[derive(Clone)]
pub struct OnTheft {
pub actor: Actor,
pub wpos: Vec3<i32>,
pub sprite: SpriteKind,
pub site: Option<SiteId>,
}
impl Event for OnTheft {}
#[derive(Clone)]
pub struct OnMountVolume {
pub actor: Actor,

View File

@ -25,7 +25,7 @@ use common::{
rtsim::{Actor, ChunkResource, NpcInput, PersonalityTrait, Profession, Role, SiteId},
spiral::Spiral2d,
store::Id,
terrain::{CoordinateConversions, TerrainChunkSize},
terrain::{sprite, CoordinateConversions, TerrainChunkSize},
time::DayPeriod,
util::Dir,
};
@ -1294,9 +1294,15 @@ fn check_inbox<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
loop {
match ctx.inbox.pop_front() {
Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => {
#[allow(clippy::single_match)]
match ctx.state.data().reports.get(report_id).map(|r| r.kind) {
Some(ReportKind::Death { killer, actor, .. })
let data = ctx.state.data();
let Some(report) = data.reports.get(report_id) else {
continue;
};
const REPORT_RESPONSE_TIME: f64 = 60.0 * 5.0;
match report.kind {
ReportKind::Death { killer, actor, .. }
if matches!(&ctx.npc.role, Role::Civilised(_)) =>
{
// TODO: Don't report self
@ -1334,15 +1340,53 @@ fn check_inbox<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
"npc-speech-witness_death"
};
ctx.known_reports.insert(report_id);
break Some(
just(move |ctx, _| {
ctx.controller.say(killer, Content::localized(phrase))
})
.l(),
);
if ctx.time_of_day.0 - report.at_tod.0 < REPORT_RESPONSE_TIME {
break Some(
just(move |ctx, _| {
ctx.controller.say(killer, Content::localized(phrase))
})
.l()
.l(),
);
}
},
Some(ReportKind::Death { .. }) => {}, // We don't care about death
None => {}, // Stale report, ignore
ReportKind::Theft {
thief,
site,
sprite,
} => {
// Check if this happened at home, where we know what belongs to who
if let Some(site) = site
&& ctx.npc.home == Some(site)
{
// TODO: Don't hardcode sentiment change.
ctx.sentiments
.toward_mut(thief)
.change_by(-0.2, Sentiment::VILLAIN);
ctx.known_reports.insert(report_id);
let phrase = if matches!(ctx.npc.profession(), Some(Profession::Farmer))
&& matches!(sprite.category(), sprite::Category::Plant)
{
"npc-speech-witness_theft_own"
} else {
"npc-speech-witness_theft"
};
if ctx.time_of_day.0 - report.at_tod.0 < REPORT_RESPONSE_TIME {
break Some(
just(move |ctx, _| {
ctx.controller.say(thief, Content::localized(phrase))
})
.r()
.l(),
);
}
}
},
// We don't care about deaths of non-civilians
ReportKind::Death { .. } => {},
}
},
Some(NpcInput::Report(_)) => {}, // Reports we already know of are ignored

View File

@ -1,6 +1,6 @@
use crate::{
data::{report::ReportKind, Report},
event::{EventCtx, OnDeath},
event::{EventCtx, OnDeath, OnTheft},
RtState, Rule, RuleError,
};
use common::rtsim::NpcInput;
@ -10,6 +10,7 @@ pub struct ReportEvents;
impl Rule for ReportEvents {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnDeath>(on_death);
rtstate.bind::<Self, OnTheft>(on_theft);
Ok(Self)
}
@ -31,7 +32,7 @@ fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
actor: ctx.event.actor,
killer: ctx.event.killer,
},
at: data.time_of_day,
at_tod: data.time_of_day,
});
// TODO: Don't push report to NPC inboxes, have a dedicated data structure that
@ -45,3 +46,30 @@ fn on_death(ctx: EventCtx<ReportEvents, OnDeath>) {
}
}
}
fn on_theft(ctx: EventCtx<ReportEvents, OnTheft>) {
let data = &mut *ctx.state.data_mut();
let nearby = data
.npcs
.nearby(None, ctx.event.wpos.as_(), 24.0)
.filter_map(|actor| actor.npc())
.collect::<Vec<_>>();
if !nearby.is_empty() {
let report = data.reports.create(Report {
kind: ReportKind::Theft {
thief: ctx.event.actor,
site: ctx.event.site,
sprite: ctx.event.sprite,
},
at_tod: data.time_of_day,
});
for npc_id in nearby {
if let Some(npc) = data.npcs.get_mut(npc_id) {
npc.inbox.push_back(NpcInput::Report(report));
}
}
}
}

View File

@ -2,7 +2,7 @@ use hashbrown::HashSet;
use rand::{seq::IteratorRandom, Rng};
use specs::{
join::Join, shred, DispatcherBuilder, Entities, Entity as EcsEntity, Read, ReadExpect,
ReadStorage, SystemData, Write, WriteStorage,
ReadStorage, SystemData, Write, WriteExpect, WriteStorage,
};
use tracing::{debug, error, warn};
use vek::{Rgb, Vec3};
@ -14,7 +14,7 @@ use common::{
item::{self, flatten_counted_items, tool::AbilityMap, MaterialStatManifest},
loot_owner::LootOwnerKind,
slot::{self, Slot},
InventoryUpdate, LootOwner, PickupItem,
InventoryUpdate, LootOwner, PickupItem, PresenceKind,
},
consts::MAX_PICKUP_RANGE,
event::{
@ -77,9 +77,12 @@ pub struct InventoryManipData<'a> {
events: Events<'a>,
block_change: Write<'a, common_state::BlockChange>,
trades: Write<'a, Trades>,
rtsim: WriteExpect<'a, crate::rtsim::RtSim>,
terrain: ReadExpect<'a, common::terrain::TerrainGrid>,
id_maps: Read<'a, IdMaps>,
time: Read<'a, Time>,
world: ReadExpect<'a, std::sync::Arc<world::World>>,
index: ReadExpect<'a, world::IndexOwned>,
program_time: ReadExpect<'a, ProgramTime>,
ability_map: ReadExpect<'a, AbilityMap>,
msm: ReadExpect<'a, MaterialStatManifest>,
@ -107,6 +110,7 @@ pub struct InventoryManipData<'a> {
pets: ReadStorage<'a, comp::Pet>,
velocities: ReadStorage<'a, comp::Vel>,
masses: ReadStorage<'a, comp::Mass>,
presences: ReadStorage<'a, comp::Presence>,
}
impl ServerEvent for InventoryManipEvent {
@ -351,6 +355,20 @@ impl ServerEvent for InventoryManipEvent {
if let Some(block) = block {
if block.is_collectible() && data.block_change.can_set_block(sprite_pos) {
if block.is_owned()
&& let Some(PresenceKind::Character(character)) =
data.presences.get(entity).map(|p| p.kind)
{
data.rtsim.hook_pickup_owned_sprite(
&data.world,
data.index.as_index_ref(),
block
.get_sprite()
.expect("If the block is owned, it is a sprite"),
sprite_pos,
common::rtsim::Actor::Character(character),
);
}
// If an item was required to collect the sprite, consume it now
if let Some((inv_slot, true)) = required_item {
inventory.take(inv_slot, &data.ability_map, &data.msm);

View File

@ -7,6 +7,7 @@ use common::{
grid::Grid,
mounting::VolumePos,
rtsim::{Actor, ChunkResource, NpcId, RtSimEntity, WorldSettings},
terrain::{CoordinateConversions, SpriteKind},
};
use common_ecs::{dispatch, System};
use common_state::BlockDiff;
@ -14,7 +15,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
use enum_map::EnumMap;
use rtsim::{
data::{npc::SimulationMode, Data, ReadError},
event::{OnDeath, OnMountVolume, OnSetup},
event::{OnDeath, OnMountVolume, OnSetup, OnTheft},
RtState,
};
use specs::DispatcherBuilder;
@ -157,6 +158,33 @@ impl RtSim {
self.state.emit(OnMountVolume { actor, pos }, world, index)
}
pub fn hook_pickup_owned_sprite(
&mut self,
world: &World,
index: IndexRef,
sprite: SpriteKind,
wpos: Vec3<i32>,
actor: Actor,
) {
let site = world.sim().get(wpos.xy().wpos_to_cpos()).and_then(|chunk| {
chunk
.sites
.iter()
.find_map(|site| self.state.data().sites.world_site_map.get(site).copied())
});
self.state.emit(
OnTheft {
actor,
wpos,
sprite,
site,
},
world,
index,
)
}
pub fn hook_load_chunk(&mut self, key: Vec2<i32>, max_res: EnumMap<ChunkResource, usize>) {
if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = Some(LoadedChunkState { max_res });

View File

@ -2046,6 +2046,7 @@ impl Hud {
vec![(
Some(GameInput::Interact),
i18n.get_msg("hud-pick_up").to_string(),
overitem::TEXT_COLOR,
)],
)
.set(overitem_id, ui_widgets);
@ -2077,17 +2078,19 @@ impl Hud {
let pos = mat.mul_point(Vec3::broadcast(0.5));
let over_pos = pos + Vec3::unit_z() * 0.7;
let interaction_text = |collect_default| match interaction {
let interaction_text = |collect_default, color| match interaction {
BlockInteraction::Collect => {
vec![(
Some(GameInput::Interact),
i18n.get_msg(collect_default).to_string(),
color,
)]
},
BlockInteraction::Craft(_) => {
vec![(
Some(GameInput::Interact),
i18n.get_msg("hud-use").to_string(),
color,
)]
},
BlockInteraction::Unlock(kind) => {
@ -2103,19 +2106,23 @@ impl Hud {
.unwrap_or_else(|| "modular item".to_string())
};
vec![(Some(GameInput::Interact), match kind {
UnlockKind::Free => i18n.get_msg("hud-open").to_string(),
UnlockKind::Requires(item_id) => i18n
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
"item" => item_name(item_id),
})
.to_string(),
UnlockKind::Consumes(item_id) => i18n
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
"item" => item_name(item_id),
})
.to_string(),
})]
vec![(
Some(GameInput::Interact),
match kind {
UnlockKind::Free => i18n.get_msg("hud-open").to_string(),
UnlockKind::Requires(item_id) => i18n
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
"item" => item_name(item_id),
})
.to_string(),
UnlockKind::Consumes(item_id) => i18n
.get_msg_ctx("hud-unlock-requires", &i18n::fluent_args! {
"item" => item_name(item_id),
})
.to_string(),
},
color,
)]
},
BlockInteraction::Mine(mine_tool) => {
match (mine_tool, &info.active_mine_tool) {
@ -2123,24 +2130,35 @@ impl Hud {
vec![(
Some(GameInput::Primary),
i18n.get_msg("hud-mine").to_string(),
color,
)]
},
(ToolKind::Pick, _) => {
vec![(None, i18n.get_msg("hud-mine-needs_pickaxe").to_string())]
vec![(
None,
i18n.get_msg("hud-mine-needs_pickaxe").to_string(),
color,
)]
},
(ToolKind::Shovel, Some(ToolKind::Shovel)) => {
vec![(
Some(GameInput::Primary),
i18n.get_msg("hud-dig").to_string(),
color,
)]
},
(ToolKind::Shovel, _) => {
vec![(None, i18n.get_msg("hud-mine-needs_shovel").to_string())]
vec![(
None,
i18n.get_msg("hud-mine-needs_shovel").to_string(),
color,
)]
},
_ => {
vec![(
None,
i18n.get_msg("hud-mine-needs_unhandled_case").to_string(),
color,
)]
},
}
@ -2156,11 +2174,12 @@ impl Hud {
) => "hud-lay",
_ => "hud-sit",
};
vec![(Some(GameInput::Mount), i18n.get_msg(key).to_string())]
vec![(Some(GameInput::Mount), i18n.get_msg(key).to_string(), color)]
},
BlockInteraction::Read(_) => vec![(
Some(GameInput::Interact),
i18n.get_msg("hud-read").to_string(),
color,
)],
// TODO: change to turn on/turn off?
BlockInteraction::LightToggle(enable) => vec![(
@ -2171,6 +2190,7 @@ impl Hud {
"hud-deactivate"
})
.to_string(),
color,
)],
};
@ -2180,6 +2200,12 @@ impl Hud {
.filter(|s| s.is_container())
.and_then(|s| get_sprite_desc(s, i18n))
{
let (text, color) = if block.is_owned() {
("hud-steal", overitem::NEGATIVE_TEXT_COLOR)
} else {
("hud-open", overitem::TEXT_COLOR)
};
overitem::Overitem::new(
desc,
overitem::TEXT_COLOR,
@ -2190,7 +2216,7 @@ impl Hud {
overitem_properties,
self.pulse,
&global_state.window.key_layout,
interaction_text("hud-open"),
interaction_text(text, color),
)
.x_y(0.0, 100.0)
.position_ingame(over_pos)
@ -2204,6 +2230,11 @@ impl Hud {
.flatten()
.next()
{
let (text, color) = if block.is_owned() {
("hud-steal", overitem::NEGATIVE_TEXT_COLOR)
} else {
("hud-collect", overitem::TEXT_COLOR)
};
item.set_amount(amount.clamp(1, item.max_amount()))
.expect("amount >= 1 and <= max_amount is always a valid amount");
make_overitem(
@ -2212,11 +2243,16 @@ impl Hud {
pos.distance_squared(player_pos),
overitem_properties,
&self.fonts,
interaction_text("hud-collect"),
interaction_text(text, color),
)
.set(overitem_id, ui_widgets);
} else if let Some(desc) = block.get_sprite().and_then(|s| get_sprite_desc(s, i18n))
{
let (text, color) = if block.is_owned() {
("hud-steal", overitem::NEGATIVE_TEXT_COLOR)
} else {
("hud-collect", overitem::TEXT_COLOR)
};
overitem::Overitem::new(
desc,
overitem::TEXT_COLOR,
@ -2227,7 +2263,7 @@ impl Hud {
overitem_properties,
self.pulse,
&global_state.window.key_layout,
interaction_text("hud-collect"),
interaction_text(text, color),
)
.x_y(0.0, 100.0)
.position_ingame(over_pos)
@ -2285,6 +2321,7 @@ impl Hud {
"hud-use"
})
.to_string(),
overitem::TEXT_COLOR,
)],
)
.x_y(0.0, 100.0)

View File

@ -15,6 +15,7 @@ use crate::hud::{CollectFailedData, HudCollectFailedReason, HudLootOwner};
use keyboard_keynames::key_layout::KeyLayout;
pub const TEXT_COLOR: Color = Color::Rgba(0.61, 0.61, 0.89, 1.0);
pub const NEGATIVE_TEXT_COLOR: Color = Color::Rgba(0.91, 0.15, 0.17, 1.0);
pub const PICKUP_FAILED_FADE_OUT_TIME: f32 = 1.5;
widget_ids! {
@ -24,7 +25,7 @@ widget_ids! {
name,
// Interaction hints
btn_bg,
btn,
btns[],
// Inventory full
inv_full_bg,
inv_full,
@ -47,7 +48,7 @@ pub struct Overitem<'a> {
pulse: f32,
key_layout: &'a Option<KeyLayout>,
// GameInput optional so we can just show stuff like "needs pickaxe"
interaction_options: Vec<(Option<GameInput>, String)>,
interaction_options: Vec<(Option<GameInput>, String, Color)>,
}
impl<'a> Overitem<'a> {
@ -61,7 +62,7 @@ impl<'a> Overitem<'a> {
properties: OveritemProperties,
pulse: f32,
key_layout: &'a Option<KeyLayout>,
interaction_options: Vec<(Option<GameInput>, String)>,
interaction_options: Vec<(Option<GameInput>, String, Color)>,
) -> Self {
Self {
name,
@ -174,42 +175,56 @@ impl<'a> Widget for Overitem<'a> {
// Interaction hints
if !self.interaction_options.is_empty() && self.properties.active {
let text = self
let texts = self
.interaction_options
.iter()
.filter_map(|(input, action)| {
.filter_map(|(input, action, color)| {
let binding = if let Some(input) = input {
Some(self.controls.get_binding(*input)?)
} else {
None
};
Some((binding, action))
Some((binding, action, color))
})
.map(|(input, action)| {
.map(|(input, action, color)| {
if let Some(input) = input {
let input = input.display_string(self.key_layout);
format!("{} {action}", input.as_str())
(format!("{} {action}", input.as_str()), color)
} else {
action.to_string()
(action.to_string(), color)
}
})
.collect::<Vec<_>>()
.join("\n");
.collect::<Vec<_>>();
if state.ids.btns.len() < texts.len() {
state.update(|state| {
state
.ids
.btns
.resize(texts.len(), &mut ui.widget_id_generator());
})
}
let hints_text = Text::new(&text)
.font_id(self.fonts.cyri.conrod_id)
.font_size(btn_font_size as u32)
.color(TEXT_COLOR)
.x_y(0.0, btn_text_pos_y)
.depth(self.distance_from_player_sqr + 1.0)
.parent(id);
let mut max_w = btn_rect_size;
let mut max_h = 0.0;
let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
for (idx, (text, color)) in texts.iter().enumerate() {
let hints_text = Text::new(text)
.font_id(self.fonts.cyri.conrod_id)
.font_size(btn_font_size as u32)
.color(**color)
.x_y(0.0, btn_text_pos_y + max_h)
.depth(self.distance_from_player_sqr + 1.0)
.parent(id);
let [w, h] = hints_text.get_wh(ui).unwrap_or([btn_rect_size; 2]);
max_w = max_w.max(w);
max_h += h;
hints_text.set(state.ids.btns[idx], ui);
}
hints_text.set(state.ids.btn, ui);
max_h = max_h.max(btn_rect_size);
RoundedRectangle::fill_with(
[w + btn_radius * 2.0, h + btn_radius * 2.0],
[max_w + btn_radius * 2.0, max_h + btn_radius * 2.0],
btn_radius,
btn_color,
)

View File

@ -233,10 +233,10 @@ impl Primitive {
#[derive(Clone)]
pub enum Fill {
Sprite(SpriteKind),
RotatedSprite(SpriteKind, u8),
RotatedSpriteWithCfg(SpriteKind, u8, SpriteCfg),
ResourceSprite(SpriteKind, u8),
Sprite(Block),
ResourceSprite(Block),
CfgSprite(Block, SpriteCfg),
Block(Block),
Brick(BlockKind, Rgb<u8>, u8),
Gradient(util::gradient::Gradient, BlockKind),
@ -248,6 +248,44 @@ pub enum Fill {
}
impl Fill {
pub fn sprite(kind: SpriteKind) -> Self { Fill::Block(Block::empty().with_sprite(kind)) }
pub fn sprite_ori(kind: SpriteKind, ori: u8) -> Self {
let block = Block::empty().with_sprite(kind);
let block = block.with_ori(ori).unwrap_or(block);
Fill::Sprite(block)
}
pub fn resource_sprite(kind: SpriteKind) -> Self {
Fill::ResourceSprite(Block::empty().with_sprite(kind))
}
pub fn resource_sprite_ori(kind: SpriteKind, ori: u8) -> Self {
let block = Block::empty().with_sprite(kind);
let block = block.with_ori(ori).unwrap_or(block);
Fill::ResourceSprite(block)
}
pub fn owned_resource_sprite_ori(kind: SpriteKind, ori: u8) -> Self {
let block = Block::empty().with_sprite(kind);
let block = block.with_ori(ori).unwrap_or(block);
let block = block
.with_attr(common::terrain::sprite::Owned(true))
.unwrap_or(block);
Fill::ResourceSprite(block)
}
pub fn sprite_ori_cfg(kind: SpriteKind, ori: u8, cfg: SpriteCfg) -> Self {
let block = Block::empty().with_sprite(kind);
let block = block.with_ori(ori).unwrap_or(block);
Fill::CfgSprite(block, cfg)
}
fn contains_at(
tree: &Store<Primitive>,
prim: Id<Primitive>,
@ -480,37 +518,22 @@ impl Fill {
) -> Option<Block> {
if Self::contains_at(tree, prim, pos, col) {
match self {
Fill::Block(block) => Some(*block),
Fill::Sprite(sprite) => Some(if old_block.is_filled() {
Block::air(*sprite)
} else {
old_block.with_sprite(*sprite)
}),
Fill::RotatedSprite(sprite, ori) | Fill::ResourceSprite(sprite, ori) => {
Fill::Sprite(sprite) | Fill::ResourceSprite(sprite) => {
Some(if old_block.is_filled() {
Block::air(*sprite)
.with_ori(*ori)
.unwrap_or_else(|| Block::air(*sprite))
*sprite
} else {
old_block
.with_sprite(*sprite)
.with_ori(*ori)
.unwrap_or_else(|| old_block.with_sprite(*sprite))
old_block.with_data_of(*sprite)
})
},
Fill::RotatedSpriteWithCfg(sprite, ori, cfg) => Some({
Fill::CfgSprite(sprite, cfg) => {
*sprite_cfg = Some(cfg.clone());
if old_block.is_filled() {
Block::air(*sprite)
.with_ori(*ori)
.unwrap_or_else(|| Block::air(*sprite))
Some(if old_block.is_filled() {
*sprite
} else {
old_block
.with_sprite(*sprite)
.with_ori(*ori)
.unwrap_or_else(|| old_block.with_sprite(*sprite))
}
}),
old_block.with_data_of(*sprite)
})
},
Fill::Block(block) => Some(*block),
Fill::Brick(bk, col, range) => Some(Block::new(
*bk,
*col + (RandomField::new(13)
@ -1080,7 +1103,7 @@ impl Painter {
min: pos,
max: pos + 1,
})
.fill(Fill::Sprite(sprite))
.fill(Fill::sprite(sprite))
}
/// Places a sprite at the provided location with the provided orientation.
@ -1089,7 +1112,7 @@ impl Painter {
min: pos,
max: pos + 1,
})
.fill(Fill::RotatedSprite(sprite, ori))
.fill(Fill::sprite_ori(sprite, ori))
}
/// Places a sprite at the provided location with the provided orientation
@ -1105,7 +1128,7 @@ impl Painter {
min: pos,
max: pos + 1,
})
.fill(Fill::RotatedSpriteWithCfg(sprite, ori, cfg))
.fill(Fill::sprite_ori_cfg(sprite, ori, cfg))
}
/// Places a sprite at the provided location with the provided orientation
@ -1116,7 +1139,18 @@ impl Painter {
min: pos,
max: pos + 1,
})
.fill(Fill::ResourceSprite(sprite, ori))
.fill(Fill::resource_sprite_ori(sprite, ori))
}
/// Places a sprite at the provided location with the provided orientation
/// which will be tracked by rtsim nature if the sprite has an associated
/// [`ChunkResource`].
pub fn owned_resource_sprite(&self, pos: Vec3<i32>, sprite: SpriteKind, ori: u8) {
self.aabb(Aabb {
min: pos,
max: pos + 1,
})
.fill(Fill::owned_resource_sprite_ori(sprite, ori))
}
/// Returns a `PrimitiveRef` of the largest pyramid with a slope of 1 that

View File

@ -671,7 +671,7 @@ fn render_tower(bridge: &Bridge, painter: &Painter, roof_kind: &RoofKind) {
1,
4,
)
.fill(Fill::Sprite(SpriteKind::FireBowlGround));
.fill(Fill::sprite(SpriteKind::FireBowlGround));
},
RoofKind::Hipped => {
painter
@ -829,7 +829,7 @@ fn render_hang(bridge: &Bridge, painter: &Painter) {
edges
.translate(Vec3::unit_z())
.fill(Fill::Sprite(SpriteKind::Rope));
.fill(Fill::sprite(SpriteKind::Rope));
edges.translate(Vec3::unit_z() * 2).fill(wood);

View File

@ -220,7 +220,7 @@ impl Structure for CliffTower {
max: (sprite_pos + 1).with_z(floor_level + 2),
})
.clear();
painter.sprite(
painter.owned_resource_sprite(
sprite_pos.with_z(floor_level + 1),
match (RandomField::new(0).get(sprite_pos.with_z(floor_level + 1)))
% 8
@ -231,6 +231,7 @@ impl Structure for CliffTower {
3 => SpriteKind::Pot,
_ => SpriteKind::MesaLantern,
},
0,
);
}
// planters
@ -504,7 +505,7 @@ impl Structure for CliffTower {
// distribute small sprites
for dir in LOCALITY {
let pos = plot_center + dir * ((length / 3) - 1);
painter.sprite(
painter.owned_resource_sprite(
pos.with_z(floor_level + 1),
match (RandomField::new(0).get(pos.with_z(floor_level)))
% 9
@ -519,6 +520,7 @@ impl Structure for CliffTower {
7 => SpriteKind::Bowl,
_ => SpriteKind::MesaLantern,
},
0,
);
}
// beds & wardrobes
@ -605,7 +607,7 @@ impl Structure for CliffTower {
SpriteKind::WallTableMesa,
(4 * d) as u8,
);
painter.rotated_sprite(
painter.owned_resource_sprite(
pos.with_z(floor_level + 4),
match (RandomField::new(0).get(pos.with_z(floor_level)))
% 3
@ -620,7 +622,7 @@ impl Structure for CliffTower {
// distribute small sprites
for dir in LOCALITY {
let pos = plot_center + dir * ((length / 3) + 1);
painter.sprite(
painter.owned_resource_sprite(
pos.with_z(floor_level + 1),
match (RandomField::new(0).get(pos.with_z(floor_level)))
% 12
@ -638,6 +640,7 @@ impl Structure for CliffTower {
10 => SpriteKind::MesaLantern,
_ => SpriteKind::FountainArabic,
},
0,
);
}
},
@ -666,7 +669,7 @@ impl Structure for CliffTower {
SpriteKind::WallTableMesa,
(4 * d) as u8,
);
painter.rotated_sprite(
painter.owned_resource_sprite(
pos.with_z(floor_level + 3),
match (RandomField::new(0).get(pos.with_z(floor_level)))
% 4
@ -682,7 +685,7 @@ impl Structure for CliffTower {
// distribute small sprites
for dir in LOCALITY {
let pos = plot_center + dir * ((length / 3) + 1);
painter.sprite(
painter.owned_resource_sprite(
pos.with_z(floor_level + 1),
match (RandomField::new(0).get(pos.with_z(floor_level)))
% 11
@ -700,6 +703,7 @@ impl Structure for CliffTower {
10 => SpriteKind::JugAndBowlArabic,
_ => SpriteKind::OvenArabic,
},
0,
);
}
},

View File

@ -364,7 +364,11 @@ impl Structure for CoastalHouse {
let sprite = sprites.swap_remove(
RandomField::new(0).get(position.with_z(base)) as usize % sprites.len(),
);
painter.sprite(position.with_z(base - 2 + (s * height)), sprite);
painter.owned_resource_sprite(
position.with_z(base - 2 + (s * height)),
sprite,
0,
);
}
}

View File

@ -489,7 +489,11 @@ impl Structure for DesertCityMultiPlot {
0 => {
for dir in NEIGHBORS {
let pos = center + dir * 4;
painter.sprite(pos.with_z(floor_level), SpriteKind::Crate);
painter.owned_resource_sprite(
pos.with_z(floor_level),
SpriteKind::Crate,
0,
);
}
for dir in NEIGHBORS {
let pos = center + dir * 8;
@ -1222,7 +1226,7 @@ impl Structure for DesertCityMultiPlot {
SpriteKind::WallTableArabic,
6 - (4 * d) as u8,
);
painter.rotated_sprite(
painter.owned_resource_sprite(
c_pos.with_z(floor_level + 1),
match (RandomField::new(0)
.get(c_pos.with_z(floor_level)))
@ -1317,7 +1321,7 @@ impl Structure for DesertCityMultiPlot {
SpriteKind::WallTableArabic,
6 - (4 * d) as u8,
);
painter.rotated_sprite(
painter.owned_resource_sprite(
a_pos.with_z(floor_level + 1),
match (RandomField::new(0)
.get(a_pos.with_z(floor_level)))

View File

@ -1,6 +1,6 @@
use super::*;
use crate::{ColumnSample, Land};
use common::terrain::{Block, BlockKind, SpriteKind};
use common::terrain::{sprite::Owned, Block, BlockKind, SpriteKind};
use rand::prelude::*;
use strum::{EnumIter, IntoEnumIterator};
use vek::*;
@ -253,7 +253,12 @@ impl Structure for FarmField {
.sprites()
.choose_weighted(rng, |(w, _)| *w)
.ok()
.and_then(|&(_, s)| Some(old.into_vacant().with_sprite(s?)))
.and_then(|&(_, s)| {
let new = old.into_vacant().with_sprite(s?);
let new = new.with_attr(Owned(true)).unwrap_or(new);
Some(new)
})
} else if z_off == 1 && rng.gen_bool(0.001) {
Some(old.into_vacant().with_sprite(SpriteKind::Scarecrow))
} else {

View File

@ -9,6 +9,7 @@ use common::{
terrain::{Block, BlockKind, SpriteKind},
};
use rand::prelude::*;
use strum::IntoEnumIterator;
use vek::*;
/// Represents house data generated by the `generate()` method
@ -1564,14 +1565,19 @@ impl Structure for House {
// drawer next to bed
painter.sprite(nightstand_pos.with_z(base), SpriteKind::DrawerSmall);
// collectible on top of drawer
let rng = RandomField::new(0).get(nightstand_pos.with_z(base + 1));
painter.sprite(nightstand_pos.with_z(base + 1), match rng % 5 {
0 => SpriteKind::Lantern,
1 => SpriteKind::PotionMinor,
2 => SpriteKind::VialEmpty,
3 => SpriteKind::Bowl,
_ => SpriteKind::Empty,
});
let rng0 = RandomField::new(0).get(nightstand_pos.with_z(base + 1));
let rng1 = RandomField::new(1).get(nightstand_pos.with_z(base + 1));
painter.owned_resource_sprite(
nightstand_pos.with_z(base + 1),
match rng0 % 5 {
0 => SpriteKind::Lantern,
1 => SpriteKind::PotionMinor,
2 => SpriteKind::VialEmpty,
3 => SpriteKind::Bowl,
_ => SpriteKind::Empty,
},
(rng1 % 4) as u8 * 2,
);
// wardrobe along wall in corner of the room
let (wardrobe_pos, drawer_ori) = match self.front {
Dir::Y => (Vec2::new(self.bounds.max.x - 2, self.bounds.min.y + 1), 4),
@ -1589,14 +1595,19 @@ impl Structure for House {
for dir in DIRS {
// random accent pieces and loot
let sprite_pos = self.bounds.center() + dir * 5;
let rng = RandomField::new(0).get(sprite_pos.with_z(base));
painter.sprite(sprite_pos.with_z(base), match rng % 32 {
0..=2 => SpriteKind::Crate,
3..=4 => SpriteKind::CoatRack,
5..=7 => SpriteKind::Pot,
8..=9 => SpriteKind::Lantern,
_ => SpriteKind::Empty,
});
let rng0 = RandomField::new(0).get(sprite_pos.with_z(base));
let rng1 = RandomField::new(1).get(sprite_pos.with_z(base));
painter.owned_resource_sprite(
sprite_pos.with_z(base),
match rng0 % 32 {
0..=2 => SpriteKind::Crate,
3..=4 => SpriteKind::CoatRack,
5..=7 => SpriteKind::Pot,
8..=9 => SpriteKind::Lantern,
_ => SpriteKind::Empty,
},
(rng1 % 4) as u8 * 2,
);
}
if self.bounds.max.x - self.bounds.min.x < 16
@ -1605,12 +1616,12 @@ impl Structure for House {
let table_pos = Vec2::new(half_x, half_y);
// room is smaller, so use small table
painter.sprite(table_pos.with_z(base), SpriteKind::TableDining);
for (idx, dir) in CARDINALS.iter().enumerate() {
let chair_pos = table_pos + dir;
for dir in Dir::iter() {
let chair_pos = table_pos + dir.to_vec2();
painter.rotated_sprite(
chair_pos.with_z(base),
SpriteKind::ChairSingle,
(idx * 2 + ((idx % 2) * 4)) as u8,
dir.opposite().sprite_ori(),
);
}
} else {
@ -1621,12 +1632,12 @@ impl Structure for House {
_ => Vec2::new(quarter_x, half_y),
};
painter.sprite(table_pos.with_z(base), SpriteKind::TableDouble);
for (idx, dir) in CARDINALS.iter().enumerate() {
let chair_pos = table_pos + dir * (1 + idx % 2) as i32;
for dir in Dir::iter() {
let chair_pos = table_pos + dir.select((2, 1)) * dir.to_vec2();
painter.rotated_sprite(
chair_pos.with_z(base),
SpriteKind::ChairSingle,
(idx * 2 + ((idx % 2) * 4)) as u8,
dir.opposite().sprite_ori(),
);
}
}

View File

@ -3,7 +3,7 @@ use crate::{
util::{RandomField, Sampler, CARDINALS, DIAGONALS},
Land,
};
use common::terrain::{BlockKind, SpriteKind};
use common::terrain::{sprite::Owned, BlockKind, SpriteKind};
use rand::prelude::*;
use std::{f32::consts::TAU, sync::Arc};
use vek::*;
@ -50,11 +50,15 @@ impl Structure for SavannahHut {
let center = self.bounds.center();
let sprite_fill = Fill::Sampling(Arc::new(|wpos| {
Some(match (RandomField::new(0).get(wpos)) % 25 {
0 => Block::air(SpriteKind::Bowl),
1 => Block::air(SpriteKind::VialEmpty),
0 => Block::air(SpriteKind::Bowl).with_attr(Owned(true)).unwrap(),
1 => Block::air(SpriteKind::VialEmpty)
.with_attr(Owned(true))
.unwrap(),
2 => Block::air(SpriteKind::Lantern),
3 => Block::air(SpriteKind::JugArabic),
4 => Block::air(SpriteKind::Crate),
4 => Block::air(SpriteKind::Crate)
.with_attr(Owned(true))
.unwrap(),
_ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
})
}));
@ -244,7 +248,7 @@ impl Structure for SavannahHut {
let sprite = sprites.swap_remove(
RandomField::new(0).get(position.with_z(base)) as usize % sprites.len(),
);
painter.sprite(position.with_z(base - 2), sprite);
painter.owned_resource_sprite(position.with_z(base - 2), sprite, 0);
}
}

View File

@ -6,7 +6,7 @@ use crate::{
};
use common::{
generation::{EntityInfo, SpecialEntity},
terrain::{BlockKind, SpriteKind},
terrain::{sprite::Owned, BlockKind, SpriteKind},
};
use rand::prelude::*;
use std::sync::Arc;
@ -43,11 +43,15 @@ impl Structure for SavannahPit {
let center = self.bounds.center();
let sprite_fill = Fill::Sampling(Arc::new(|wpos| {
Some(match (RandomField::new(0).get(wpos)) % 50 {
0 => Block::air(SpriteKind::Bowl),
1 => Block::air(SpriteKind::VialEmpty),
0 => Block::air(SpriteKind::Bowl).with_attr(Owned(true)).unwrap(),
1 => Block::air(SpriteKind::VialEmpty)
.with_attr(Owned(true))
.unwrap(),
2 => Block::air(SpriteKind::Lantern),
3 => Block::air(SpriteKind::JugArabic),
4 => Block::air(SpriteKind::Crate),
4 => Block::air(SpriteKind::Crate)
.with_attr(Owned(true))
.unwrap(),
_ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
})
}));
@ -727,7 +731,11 @@ impl Structure for SavannahPit {
RandomField::new(0).get(position.with_z(base)) as usize
% sprites.len(),
);
painter.sprite(position.with_z(base - ((1 + f) * length)), sprite);
painter.owned_resource_sprite(
position.with_z(base - ((1 + f) * length)),
sprite,
0,
);
}
}
},

View File

@ -5,7 +5,7 @@ use crate::{
};
use common::{
generation::SpecialEntity,
terrain::{BlockKind, SpriteKind},
terrain::{sprite::Owned, BlockKind, SpriteKind},
};
use rand::prelude::*;
use std::{f32::consts::TAU, sync::Arc};
@ -53,11 +53,15 @@ impl Structure for SavannahWorkshop {
let center = self.bounds.center();
let sprite_fill = Fill::Sampling(Arc::new(|wpos| {
Some(match (RandomField::new(0).get(wpos)) % 25 {
0 => Block::air(SpriteKind::Bowl),
1 => Block::air(SpriteKind::VialEmpty),
0 => Block::air(SpriteKind::Bowl).with_attr(Owned(true)).unwrap(),
1 => Block::air(SpriteKind::VialEmpty)
.with_attr(Owned(true))
.unwrap(),
2 => Block::air(SpriteKind::Lantern),
3 => Block::air(SpriteKind::JugArabic),
4 => Block::air(SpriteKind::Crate),
4 => Block::air(SpriteKind::Crate)
.with_attr(Owned(true))
.unwrap(),
_ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
})
}));

View File

@ -1491,10 +1491,7 @@ impl Structure for Tavern {
max: (wall_center + in_dir.rotated_cw().to_vec2())
.with_z(wall.base_alt + 2),
}))
.fill(Fill::RotatedSprite(
SpriteKind::Window1,
in_dir.sprite_ori(),
));
.fill(Fill::sprite_ori(SpriteKind::Window1, in_dir.sprite_ori()));
}
},
}