diff --git a/rmxp-types/src/lib.rs b/rmxp-types/src/lib.rs
index 753e56ed..c694fe45 100644
--- a/rmxp-types/src/lib.rs
+++ b/rmxp-types/src/lib.rs
@@ -7,11 +7,14 @@ pub mod rmxp;
// Shared structs with the same layout
mod shared;
+mod option_vec;
+
mod rgss_structs;
mod helpers;
pub use helpers::*;
+pub use option_vec::OptionVec;
pub use rgss_structs::{Color, Table1, Table2, Table3, Tone};
pub mod rpg {
diff --git a/rmxp-types/src/option_vec.rs b/rmxp-types/src/option_vec.rs
new file mode 100644
index 00000000..76900db5
--- /dev/null
+++ b/rmxp-types/src/option_vec.rs
@@ -0,0 +1,248 @@
+// Copyright (C) 2023 Lily Lyons
+//
+// This file is part of Luminol.
+//
+// Luminol is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Luminol is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Luminol. If not, see .
+
+use std::ops::{Index, IndexMut};
+
+use serde::ser::SerializeMap;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+/// A vector that can contain unused indices.
+pub struct OptionVec {
+ vec: Vec>,
+ num_values: usize,
+}
+
+pub struct Iter<'a, T> {
+ vec_iter: std::iter::Enumerate>>,
+}
+
+pub struct IterMut<'a, T> {
+ vec_iter: std::iter::Enumerate>>,
+}
+
+pub struct Visitor(std::marker::PhantomData);
+
+impl OptionVec {
+ /// Create a new OptionVec with no elements.
+ pub fn new() -> Self {
+ Self {
+ vec: Vec::new(),
+ num_values: 0,
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.vec.len()
+ }
+
+ pub fn size(&self) -> usize {
+ self.num_values
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.vec.is_empty()
+ }
+
+ pub fn get(&self, index: usize) -> Option<&T> {
+ self.vec.get(index).and_then(|x| x.as_ref())
+ }
+
+ pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
+ self.vec.get_mut(index).and_then(|x| x.as_mut())
+ }
+
+ pub fn capacity(&self) -> usize {
+ self.vec.capacity()
+ }
+
+ pub fn reserve(&mut self, additional: usize) {
+ self.vec.reserve(additional);
+ }
+
+ pub fn iter(&self) -> Iter<'_, T> {
+ self.into_iter()
+ }
+
+ pub fn iter_mut(&mut self) -> IterMut<'_, T> {
+ self.into_iter()
+ }
+
+ /// Write the element at the given index.
+ /// If there is already an element at the given index, it will be overwritten.
+ /// If there isn't, a new element will be added at that index.
+ pub fn insert(&mut self, index: usize, element: T) {
+ if index >= self.len() {
+ let additional = index - self.len() + 1;
+ self.reserve(additional);
+ self.vec
+ .extend(std::iter::repeat_with(|| None).take(additional));
+ }
+ if self.vec[index].is_none() {
+ self.num_values += 1;
+ }
+ self.vec[index] = Some(element);
+ }
+
+ /// Remove the element at the given index.
+ /// If the OptionVec is not big enough to contain this index, this will throw an error.
+ /// If there isn't an element at that index, this will throw an error.
+ pub fn try_remove(&mut self, index: usize) -> Result<(), String> {
+ if index >= self.len() {
+ Err(String::from("index out of bounds"))
+ } else if self.vec[index].is_none() {
+ Err(String::from("index not found"))
+ } else {
+ self.num_values -= 1;
+ self.vec[index] = None;
+ Ok(())
+ }
+ }
+
+ /// Remove the element at the given index.
+ /// If the OptionVec is not big enough to contain this index, this will panic.
+ /// If there isn't an element at that index, this will panic.
+ pub fn remove(&mut self, index: usize) {
+ self.try_remove(index).unwrap()
+ }
+}
+
+impl Default for OptionVec {
+ fn default() -> Self {
+ OptionVec::new()
+ }
+}
+
+impl FromIterator<(usize, T)> for OptionVec {
+ fn from_iter>(iterable: I) -> Self {
+ let mut vec = Vec::new();
+ let mut num_values = 0;
+ for (i, v) in iterable.into_iter() {
+ if i >= vec.len() {
+ let additional = i - vec.len() + 1;
+ vec.reserve(additional);
+ vec.extend(std::iter::repeat_with(|| None).take(additional));
+ }
+ vec[i] = Some(v);
+ num_values += 1;
+ }
+ Self { vec, num_values }
+ }
+}
+
+impl Index for OptionVec {
+ type Output = T;
+ fn index(&self, index: usize) -> &Self::Output {
+ self.get(index).expect("index not found")
+ }
+}
+
+impl IndexMut for OptionVec {
+ fn index_mut(&mut self, index: usize) -> &mut Self::Output {
+ self.get_mut(index).expect("index not found")
+ }
+}
+
+impl<'a, T> IntoIterator for &'a OptionVec {
+ type Item = (usize, &'a T);
+ type IntoIter = Iter<'a, T>;
+ fn into_iter(self) -> Self::IntoIter {
+ Self::IntoIter {
+ vec_iter: self.vec.iter().enumerate(),
+ }
+ }
+}
+
+impl<'a, T> IntoIterator for &'a mut OptionVec {
+ type Item = (usize, &'a mut T);
+ type IntoIter = IterMut<'a, T>;
+ fn into_iter(self) -> Self::IntoIter {
+ Self::IntoIter {
+ vec_iter: self.vec.iter_mut().enumerate(),
+ }
+ }
+}
+
+impl<'a, T> Iterator for Iter<'a, T> {
+ type Item = (usize, &'a T);
+ fn next(&mut self) -> Option {
+ for (index, element) in &mut self.vec_iter {
+ if let Some(element) = element {
+ return Some((index, element));
+ }
+ }
+ None
+ }
+}
+
+impl<'a, T> Iterator for IterMut<'a, T> {
+ type Item = (usize, &'a mut T);
+ fn next(&mut self) -> Option {
+ for (index, element) in &mut self.vec_iter {
+ if let Some(element) = element {
+ return Some((index, element));
+ }
+ }
+ None
+ }
+}
+
+impl<'de, T> serde::de::Visitor<'de> for Visitor
+where
+ T: serde::Deserialize<'de>,
+{
+ type Value = OptionVec;
+
+ fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+ formatter.write_str("a key-value mapping")
+ }
+
+ fn visit_map(self, mut map: A) -> Result
+ where
+ A: serde::de::MapAccess<'de>,
+ {
+ std::iter::from_fn(|| map.next_entry().transpose()).collect()
+ }
+}
+
+impl<'de, T> serde::Deserialize<'de> for OptionVec
+where
+ T: serde::Deserialize<'de>,
+{
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ deserializer.deserialize_map(Visitor(std::marker::PhantomData))
+ }
+}
+
+impl serde::Serialize for OptionVec
+where
+ T: serde::Serialize,
+{
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ let mut ser = serializer.serialize_map(Some(self.size()))?;
+ for (index, element) in self {
+ ser.serialize_key(&index)?;
+ ser.serialize_value(element)?;
+ }
+ ser.end()
+ }
+}
diff --git a/rmxp-types/src/rmxp/map.rs b/rmxp-types/src/rmxp/map.rs
index 975b39ef..6af02c07 100644
--- a/rmxp-types/src/rmxp/map.rs
+++ b/rmxp-types/src/rmxp/map.rs
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Luminol. If not, see .
use crate::rpg::{AudioFile, Event};
-use crate::{id, Table3};
+use crate::{id, option_vec, Table3};
#[derive(Default, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename = "RPG::Map")]
@@ -31,5 +31,5 @@ pub struct Map {
pub encounter_list: Vec,
pub encounter_step: i32,
pub data: Table3,
- pub events: slab::Slab,
+ pub events: option_vec::OptionVec,
}
diff --git a/rmxp-types/src/shared/event.rs b/rmxp-types/src/shared/event.rs
index 5c07f2cb..3192ec2b 100644
--- a/rmxp-types/src/shared/event.rs
+++ b/rmxp-types/src/shared/event.rs
@@ -25,6 +25,15 @@ pub struct Event {
pub x: i32,
pub y: i32,
pub pages: Vec,
+
+ #[serde(skip)]
+ pub extra_data: EventExtraData,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct EventExtraData {
+ /// Whether or not the event editor for this event is open
+ pub is_editor_open: bool,
}
impl Event {
@@ -36,6 +45,8 @@ impl Event {
x,
y,
pages: vec![EventPage::default()],
+
+ extra_data: EventExtraData::default(),
}
}
}
diff --git a/src/components/map_view.rs b/src/components/map_view.rs
index 53cbce3e..e0854178 100644
--- a/src/components/map_view.rs
+++ b/src/components/map_view.rs
@@ -26,14 +26,23 @@ pub struct MapView {
pub pan: egui::Vec2,
pub inter_tile_pan: egui::Vec2,
- pub events: HashMap,
+ pub events: rmxp_types::OptionVec,
pub map: Map,
pub selected_layer: SelectedLayer,
+ pub selected_event_id: Option,
pub cursor_pos: egui::Pos2,
pub event_enabled: bool,
pub snap_to_grid: bool,
+ /// The map coordinates of the tile being hovered over
+ pub hover_tile: Option,
+
+ /// True if selected_event_id is being hovered over by the mouse
+ /// (as opposed to the map cursor)
+ /// and false otherwise
+ pub selected_event_is_hovered: bool,
+
pub darken_unselected_layers: bool,
pub scale: f32,
@@ -70,12 +79,17 @@ impl MapView {
map,
selected_layer: SelectedLayer::default(),
+ selected_event_id: None,
cursor_pos: egui::Pos2::ZERO,
event_enabled: true,
snap_to_grid: false,
darken_unselected_layers: true,
+ hover_tile: None,
+
+ selected_event_is_hovered: false,
+
scale: 100.,
})
}
@@ -142,12 +156,14 @@ impl MapView {
let canvas_pos = canvas_center + self.pan;
// We check here after we calculate the scale and whatnot
+ self.hover_tile = None;
if let Some(pos) = response.hover_pos() {
let mut pos_tile = (pos - self.pan - canvas_center) / tile_size
+ egui::Vec2::new(map.width as f32 / 2., map.height as f32 / 2.);
// Force the cursor to a tile instead of in-between
pos_tile.x = pos_tile.x.floor().clamp(0., map.width as f32 - 1.);
pos_tile.y = pos_tile.y.floor().clamp(0., map.height as f32 - 1.);
+ self.hover_tile = Some(pos_tile.to_pos2());
// Handle input
if matches!(self.selected_layer, SelectedLayer::Tiles(_))
|| dragging_event
@@ -184,14 +200,45 @@ impl MapView {
egui::Stroke::new(3., egui::Color32::DARK_GRAY),
);
+ let cursor_rect = egui::Rect::from_min_size(
+ map_rect.min + (self.cursor_pos.to_vec2() * tile_size),
+ egui::Vec2::splat(tile_size),
+ );
+
+ if !self.event_enabled || !matches!(self.selected_layer, SelectedLayer::Events) {
+ self.selected_event_id = None;
+ }
+ self.selected_event_is_hovered = false;
+
if self.event_enabled {
+ let mut selected_event = None;
+ let mut selected_event_rects = None;
+
for (_, event) in map.events.iter() {
- let sprite = self.events.get(&event.id);
+ let sprite = self.events.get(event.id);
let event_size = sprite
.map(|e| e.sprite_size)
.unwrap_or(egui::vec2(32., 32.));
let scaled_event_size = event_size * scale;
+ // Darken the graphic if required
+ if let Some(sprite) = sprite {
+ sprite.sprite().graphic.set_opacity_multiplier(
+ if self.darken_unselected_layers
+ && !matches!(self.selected_layer, SelectedLayer::Events)
+ {
+ 0.5
+ } else {
+ 1.
+ },
+ );
+ }
+
+ let tile_rect = egui::Rect::from_min_size(
+ map_rect.min
+ + egui::vec2(event.x as f32 * tile_size, event.y as f32 * tile_size),
+ egui::vec2(32., 32.) * scale,
+ );
let box_rect = egui::Rect::from_min_size(
map_rect.min
+ egui::vec2(
@@ -205,26 +252,143 @@ impl MapView {
sprite.paint(ui.painter(), box_rect);
}
- ui.painter()
- .rect_stroke(box_rect, 5., egui::Stroke::new(1., egui::Color32::WHITE));
-
- if ui.rect_contains_pointer(box_rect) {
- response = response.on_hover_ui_at_pointer(|ui| {
- ui.label(format!("Event {:0>3}: {:?}", event.id, event.name));
+ if matches!(self.selected_layer, SelectedLayer::Events)
+ && ui.input(|i| !i.modifiers.shift)
+ {
+ ui.painter().rect_stroke(
+ box_rect,
+ 5.,
+ egui::Stroke::new(1., egui::Color32::WHITE),
+ );
+
+ // If the mouse is not hovering over an event, then we will handle the selected
+ // tile based on where the map cursor is
+ if !self.selected_event_is_hovered && !dragging_event {
+ selected_event = match selected_event {
+ // If the map cursor is on the exact tile of an event, then that is the
+ // selected event
+ Some(_)
+ if self.cursor_pos.x == event.x as f32
+ && self.cursor_pos.y == event.y as f32 =>
+ {
+ Some(event)
+ }
+ Some(e)
+ if self.cursor_pos.x == e.x as f32
+ && self.cursor_pos.y == e.y as f32 =>
+ {
+ selected_event
+ }
+ // Otherwise if the map cursor intersects at least one event's graphic,
+ // then the one out of those with the highest ID should be the selected
+ // event
+ _ if box_rect.contains(cursor_rect.center()) => Some(event),
+ _ => selected_event,
+ };
+ if let Some(e) = selected_event {
+ if e.id == event.id {
+ selected_event_rects = Some((tile_rect, box_rect));
+ }
+ }
+ }
+
+ if ui.rect_contains_pointer(box_rect) {
+ response = response.on_hover_ui_at_pointer(|ui| {
+ ui.label(format!("Event {:0>3}: {:?}", event.id, event.name));
+
+ let (response, painter) = ui.allocate_painter(
+ event_size * ui.ctx().pixels_per_point(),
+ egui::Sense::click(),
+ );
+ if let Some(sprite) = sprite {
+ sprite.paint(&painter, response.rect);
+ }
+ match self.selected_event_id {
+ Some(id) if id == event.id => ui.painter().rect_stroke(
+ response.rect,
+ 5.,
+ egui::Stroke::new(2., egui::Color32::YELLOW),
+ ),
+ _ => ui.painter().rect_stroke(
+ response.rect,
+ 5.,
+ egui::Stroke::new(1., egui::Color32::WHITE),
+ ),
+ }
+ });
+
+ if let Some(hover_tile) = self.hover_tile {
+ if !dragging_event {
+ // Handle which event should be considered selected based on the
+ // hovered tile
+ selected_event = match selected_event {
+ // If the cursor is hovering over the exact tile of an event, then that is
+ // the selected event
+ Some(_)
+ if hover_tile.x == event.x as f32
+ && hover_tile.y == event.y as f32 =>
+ {
+ Some(event)
+ }
+ Some(e)
+ if hover_tile.x == e.x as f32
+ && hover_tile.y == e.y as f32 =>
+ {
+ selected_event
+ }
+ // Otherwise if the cursor is hovering over at least one event's graphic,
+ // then the one out of those with the highest ID should be the selected event
+ _ => Some(event),
+ };
+ if let Some(e) = selected_event {
+ if e.id == event.id {
+ self.selected_event_is_hovered = true;
+ selected_event_rects = Some((tile_rect, box_rect));
+ }
+ }
+ }
+ }
+ }
- let (response, painter) = ui.allocate_painter(
- event_size * ui.ctx().pixels_per_point(),
- egui::Sense::click(),
- );
- if let Some(sprite) = sprite {
- sprite.paint(&painter, response.rect);
+ // If an event is being dragged, that should always be the selected event
+ if let Some(id) = self.selected_event_id {
+ if dragging_event && id == event.id {
+ selected_event = Some(event);
+ selected_event_rects = Some((tile_rect, box_rect));
}
+ }
+ } else {
+ ui.painter().rect_stroke(
+ box_rect,
+ 5.,
+ egui::Stroke::new(1., egui::Color32::DARK_GRAY),
+ );
+ }
+
+ // Draw a magenta rectangle on the border of events that are being edited
+ if event.extra_data.is_editor_open {
+ ui.painter().rect_stroke(
+ box_rect,
+ 5.,
+ egui::Stroke::new(3., egui::Color32::from_rgb(255, 0, 255)),
+ );
+ }
+ }
+
+ self.selected_event_id = selected_event.map(|e| e.id);
+
+ // Draw a yellow rectangle on the border of the selected event's graphic
+ if let Some(selected_event) = selected_event {
+ // Make sure the event editor isn't open so we don't draw over the
+ // magenta rectangle
+ if !selected_event.extra_data.is_editor_open {
+ if let Some((tile_rect, box_rect)) = selected_event_rects {
ui.painter().rect_stroke(
- response.rect,
+ box_rect,
5.,
- egui::Stroke::new(1., egui::Color32::WHITE),
+ egui::Stroke::new(3., egui::Color32::YELLOW),
);
- });
+ }
}
}
}
@@ -250,10 +414,6 @@ impl MapView {
}
// Display cursor.
- let cursor_rect = egui::Rect::from_min_size(
- map_rect.min + (self.cursor_pos.to_vec2() * tile_size),
- egui::Vec2::splat(tile_size),
- );
ui.painter().rect_stroke(
cursor_rect,
5.,
diff --git a/src/graphics/event.rs b/src/graphics/event.rs
index 03031e4a..b402b9de 100644
--- a/src/graphics/event.rs
+++ b/src/graphics/event.rs
@@ -67,12 +67,13 @@ impl Event {
egui::vec2(cw, ch),
);
+ // Reduced by 0.01 px on all sides to reduce texture bleeding
let tex_coords = egui::Rect::from_min_size(
egui::pos2(
- page.graphic.pattern as f32 * cw,
- (page.graphic.direction as f32 - 2.) / 2. * ch,
+ page.graphic.pattern as f32 * cw + 0.01,
+ (page.graphic.direction as f32 - 2.) / 2. * ch + 0.01,
),
- egui::vec2(cw, ch),
+ egui::vec2(cw - 0.02, ch - 0.02),
);
let quad = primitives::Quad::new(pos, tex_coords, 0.0);
@@ -96,6 +97,10 @@ impl Event {
}))
}
+ pub fn sprite(&self) -> &primitives::Sprite {
+ &self.resources.sprite
+ }
+
pub fn paint(&self, painter: &egui::Painter, rect: egui::Rect) {
let resources = self.resources.clone();
let resource_id = Arc::new(OnceCell::new());
diff --git a/src/graphics/primitives/sprite/graphic.rs b/src/graphics/primitives/sprite/graphic.rs
index c8ebbfd4..3b899092 100644
--- a/src/graphics/primitives/sprite/graphic.rs
+++ b/src/graphics/primitives/sprite/graphic.rs
@@ -27,13 +27,18 @@ pub struct Graphic {
struct Data {
hue: f32,
opacity: f32,
+ opacity_multiplier: f32,
}
impl Graphic {
pub fn new(hue: i32, opacity: i32) -> Self {
let hue = (hue % 360) as f32 / 360.0;
let opacity = opacity as f32 / 255.;
- let data = Data { hue, opacity };
+ let data = Data {
+ hue,
+ opacity,
+ opacity_multiplier: 1.,
+ };
Self {
data: AtomicCell::new(data),
@@ -62,6 +67,19 @@ impl Graphic {
self.data.store(Data { opacity, ..data });
}
+ pub fn opacity_multiplier(&self) -> f32 {
+ self.data.load().opacity_multiplier
+ }
+
+ pub fn set_opacity_multiplier(&self, opacity_multiplier: f32) {
+ let data = self.data.load();
+
+ self.data.store(Data {
+ opacity_multiplier,
+ ..data
+ });
+ }
+
pub fn as_bytes(&self) -> [u8; std::mem::size_of::()] {
bytemuck::cast(self.data.load())
}
diff --git a/src/graphics/primitives/sprite/shader.rs b/src/graphics/primitives/sprite/shader.rs
index 4b7af3fc..370c0d72 100644
--- a/src/graphics/primitives/sprite/shader.rs
+++ b/src/graphics/primitives/sprite/shader.rs
@@ -44,7 +44,7 @@ impl Shader {
},
wgpu::PushConstantRange {
stages: wgpu::ShaderStages::FRAGMENT,
- range: 64..(64 + 8),
+ range: 64..(64 + 4 + 4 + 4),
},
],
});
diff --git a/src/graphics/primitives/sprite/sprite.wgsl b/src/graphics/primitives/sprite/sprite.wgsl
index 2f7dcb32..aac20d2d 100644
--- a/src/graphics/primitives/sprite/sprite.wgsl
+++ b/src/graphics/primitives/sprite/sprite.wgsl
@@ -20,7 +20,8 @@ struct Viewport {
struct Graphic {
hue: f32,
- opacity: f32
+ opacity: f32,
+ opacity_multiplier: f32,
}
var push_constants: PushConstants;
@@ -68,7 +69,7 @@ fn vs_main(
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4 {
var tex_sample = textureSample(t_diffuse, s_diffuse, in.tex_coords);
- tex_sample.a *= push_constants.graphic.opacity;
+ tex_sample.a *= push_constants.graphic.opacity * push_constants.graphic.opacity_multiplier;
if tex_sample.a <= 0. {
discard;
}
@@ -81,4 +82,4 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 {
}
return tex_sample;
-}
\ No newline at end of file
+}
diff --git a/src/graphics/primitives/tiles/atlas.rs b/src/graphics/primitives/tiles/atlas.rs
index 5f240a79..4aca71f8 100644
--- a/src/graphics/primitives/tiles/atlas.rs
+++ b/src/graphics/primitives/tiles/atlas.rs
@@ -295,9 +295,10 @@ impl Atlas {
egui::pos2(0., 0.),
egui::vec2(TILE_SIZE as f32, TILE_SIZE as f32),
),
+ // Reduced by 0.01 px on all sides to decrease texture bleeding
egui::Rect::from_min_size(
- atlas_tile_position,
- egui::vec2(TILE_SIZE as f32, TILE_SIZE as f32),
+ atlas_tile_position + egui::vec2(0.01, 0.01),
+ egui::vec2(TILE_SIZE as f32 - 0.02, TILE_SIZE as f32 - 0.02),
),
0.0,
)
diff --git a/src/tabs/map.rs b/src/tabs/map.rs
index df006dae..c85f36fb 100644
--- a/src/tabs/map.rs
+++ b/src/tabs/map.rs
@@ -38,6 +38,10 @@ pub struct Tab {
dragging_event: bool,
event_windows: window::Windows,
force_close: bool,
+
+ /// When event dragging starts, this is set to the difference between
+ /// the dragged event's tile and the cursor position
+ event_drag_offset: Option,
}
impl Tab {
@@ -55,6 +59,8 @@ impl Tab {
dragging_event: false,
event_windows: window::Windows::default(),
force_close: false,
+
+ event_drag_offset: None,
})
}
@@ -207,6 +213,53 @@ impl Tab {
}
}
}
+
+ fn add_event(&self, map: &mut rpg::Map) {
+ let mut first_vacant_id = 1;
+ let mut max_event_id = 0;
+
+ for (_, event) in map.events.iter() {
+ if event.id == first_vacant_id {
+ first_vacant_id += 1;
+ }
+ max_event_id = event.id;
+
+ if event.x == self.view.cursor_pos.x as i32 && event.y == self.view.cursor_pos.y as i32
+ {
+ state!()
+ .toasts
+ .error("Cannot create event on an existing event's tile");
+ return;
+ }
+ }
+
+ // Try first to allocate the event number directly after the current highest one.
+ // However, valid event number range in RPG Maker XP and VX is 1-999.
+ let new_event_id = if max_event_id < 999 {
+ max_event_id + 1
+ }
+ // Otherwise, we'll try to use a non-allocated event ID that isn't zero.
+ else if first_vacant_id <= 999 {
+ first_vacant_id
+ } else {
+ state!()
+ .toasts
+ .error("Event limit reached, please delete some events");
+ return;
+ };
+
+ map.events.insert(
+ new_event_id,
+ rpg::Event::new(
+ self.view.cursor_pos.x as i32,
+ self.view.cursor_pos.y as i32,
+ new_event_id,
+ ),
+ );
+
+ self.event_windows
+ .add_window(event_edit::Window::new(new_event_id, self.id));
+ }
}
impl tab::Tab for Tab {
@@ -224,11 +277,6 @@ impl tab::Tab for Tab {
}
fn show(&mut self, ui: &mut egui::Ui) {
- let state = state!();
-
- // Get the map.
- let mut map = state.data_cache.map(self.id);
-
// Display the toolbar.
egui::TopBottomPanel::top(format!("map_{}_toolbar", self.id)).show_inside(ui, |ui| {
ui.horizontal_wrapped(|ui| {
@@ -326,12 +374,20 @@ impl tab::Tab for Tab {
egui::CentralPanel::default().show_inside(ui, |ui| {
egui::Frame::canvas(ui.style()).show(ui, |ui| {
+ // Get the map.
+ let mut map = state!().data_cache.map(self.id);
+
let response = self.view.ui(ui, &map, self.dragging_event);
let layers_max = map.data.zsize();
let map_x = self.view.cursor_pos.x as i32;
let map_y = self.view.cursor_pos.y as i32;
+ if self.dragging_event && self.view.selected_event_id.is_none() {
+ self.dragging_event = false;
+ self.event_drag_offset = None;
+ }
+
if let SelectedLayer::Tiles(tile_layer) = self.view.selected_layer {
if response.dragged_by(egui::PointerButton::Primary)
&& !ui.input(|i| i.modifiers.command)
@@ -342,19 +398,96 @@ impl tab::Tab for Tab {
(map_x as usize, map_y as usize, tile_layer),
);
}
- }
+ } else if let Some(selected_event_id) = self.view.selected_event_id {
+ if response.double_clicked()
+ || (response.hovered()
+ && ui.memory(|m| m.focus().is_none())
+ && ui.input(|i| i.key_pressed(egui::Key::Enter)))
+ {
+ // Double-click/press enter on events to edit them
+ if ui.input(|i| !i.modifiers.command) {
+ self.dragging_event = false;
+ self.event_drag_offset = None;
+ self.event_windows
+ .add_window(event_edit::Window::new(selected_event_id, self.id));
+ }
+ }
+ // Allow drag and drop to move events
+ else if !self.dragging_event
+ && self.view.selected_event_is_hovered
+ && response.drag_started_by(egui::PointerButton::Primary)
+ {
+ self.dragging_event = true;
+ } else if self.dragging_event
+ && !response.dragged_by(egui::PointerButton::Primary)
+ {
+ self.dragging_event = false;
+ self.event_drag_offset = None;
+ }
+
+ // Press delete or backspace to delete the selected event
+ if response.hovered()
+ && ui.memory(|m| m.focus().is_none())
+ && ui.input(|i| {
+ i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)
+ })
+ {
+ map.events.remove(selected_event_id);
+ let _ = self.view.events.try_remove(selected_event_id);
+ }
+
+ if let Some(hover_tile) = self.view.hover_tile {
+ if self.dragging_event {
+ if let Some(selected_event) = map.events.get(selected_event_id) {
+ // If we just started dragging an event, save the offset between the
+ // cursor and the event's tile so that the event will be dragged
+ // with that offset from the cursor
+ if self.event_drag_offset.is_none() {
+ self.event_drag_offset = Some(
+ egui::Pos2::new(
+ selected_event.x as f32,
+ selected_event.y as f32,
+ ) - hover_tile,
+ );
+ };
+ }
- if ui.input(|i| {
- i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)
- }) {
- if let Some((id, _)) = map
- .events
- .iter()
- .find(|(_, event)| event.x == map_x && event.y == map_y)
+ if let Some(offset) = self.event_drag_offset {
+ // If moving an event, move the dragged event's tile to the cursor
+ // after adjusting for drag offset, unless that would put the event
+ // on the same tile as an existing event
+ let adjusted_hover_tile = hover_tile + offset;
+ if !map.events.iter().any(|(_, e)| {
+ adjusted_hover_tile.x == e.x as f32
+ && adjusted_hover_tile.y == e.y as f32
+ }) {
+ if let Some(selected_event) =
+ map.events.get_mut(selected_event_id)
+ {
+ selected_event.x = adjusted_hover_tile.x as i32;
+ selected_event.y = adjusted_hover_tile.y as i32;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ // Double-click/press enter on an empty space to add an event
+ // (hold shift to prevent events from being selected)
+ if response.double_clicked()
+ || (response.hovered()
+ && ui.memory(|m| m.focus().is_none())
+ && ui.input(|i| i.key_pressed(egui::Key::Enter)))
{
- map.events.remove(id);
+ self.dragging_event = false;
+ self.event_drag_offset = None;
+ self.add_event(&mut map);
}
}
+
+ for (_, event) in map.events.iter_mut() {
+ event.extra_data.is_editor_open = false;
+ }
})
});
diff --git a/src/windows/event_edit.rs b/src/windows/event_edit.rs
index f1c3ffa2..cf740f20 100644
--- a/src/windows/event_edit.rs
+++ b/src/windows/event_edit.rs
@@ -29,19 +29,19 @@ pub struct Window {
id: usize,
map_id: usize,
selected_page: usize,
- event: rpg::Event,
+ name: String,
viewed_tab: u8,
modals: (bool, bool, bool),
}
impl Window {
/// Create a new event editor.
- pub fn new(id: usize, map_id: usize, event: rpg::Event, tileset_name: String) -> Self {
+ pub fn new(id: usize, map_id: usize) -> Self {
Self {
id,
map_id,
selected_page: 0,
- event,
+ name: String::from("(unknown)"),
viewed_tab: 2,
modals: (false, false, false),
}
@@ -50,17 +50,27 @@ impl Window {
impl window::Window for Window {
fn name(&self) -> String {
- format!(
- "Event: {}, {} in Map {}",
- self.event.name, self.id, self.map_id
- )
+ format!("Event: {}, {} in Map {}", self.name, self.id, self.map_id)
}
fn id(&self) -> egui::Id {
- egui::Id::new("Event Editor")
+ egui::Id::new("luminol_event_edit")
+ .with(self.map_id)
+ .with(self.id)
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
+ let mut map = state!().data_cache.map(self.map_id);
+ let event = match map.events.get_mut(self.id) {
+ Some(e) => e,
+ None => {
+ *open = false;
+ return;
+ }
+ };
+ event.extra_data.is_editor_open = true;
+ self.name.clone_from(&event.name);
+
let mut win_open = true;
egui::Window::new(self.name())
@@ -68,7 +78,7 @@ impl window::Window for Window {
.open(&mut win_open)
.show(ctx, |ui| {
ui.horizontal(|ui| {
- ui.text_edit_singleline(&mut self.event.name);
+ ui.text_edit_singleline(&mut event.name);
ui.button("New page").clicked();
ui.button("Copy page").clicked();
@@ -79,7 +89,7 @@ impl window::Window for Window {
ui.separator();
ui.horizontal(|ui| {
- for (page, _) in self.event.pages.iter().enumerate() {
+ for (page, _) in event.pages.iter().enumerate() {
if ui
.selectable_value(&mut self.selected_page, page, page.to_string())
.clicked()
@@ -99,7 +109,7 @@ impl window::Window for Window {
ui.separator();
- let page = self.event.pages.get_mut(self.selected_page).unwrap();
+ let page = event.pages.get_mut(self.selected_page).unwrap();
match self.viewed_tab {
0 => {
@@ -282,7 +292,9 @@ impl window::Window for Window {
});
});
}
+
1 => {}
+
2 => {
ui.vertical(|ui| {
ui.group(|ui| {
@@ -307,8 +319,8 @@ impl window::Window for Window {
let cancel_clicked = ui.button("Cancel").clicked();
if apply_clicked || ok_clicked {
- let mut map = state!().data_cache.map(self.map_id);
- map.events[self.id] = self.event.clone();
+ //let mut map = state!().data_cache.map(self.map_id);
+ //map.events[self.id] = event.clone();
}
if cancel_clicked || ok_clicked {