palette_creature.cpp

main 2167 lines · 67.6 KB Raw
Alex Alex Commit Map Editor 18/10/2026 23:40
1//////////////////////////////////////////////////////////////////////
2// This file is part of Remere's Map Editor
3//////////////////////////////////////////////////////////////////////
4// Remere's Map Editor is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Remere's Map Editor is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//////////////////////////////////////////////////////////////////////
17
18#include "main.h"
19
20#include "settings.h"
21#include "brush.h"
22#include "gui.h"
23#include "palette_creature.h"
24#include "creature_brush.h"
25#include "spawn_brush.h"
26#include "materials.h"
27#include "add_creature_dialog.h"
28#include <wx/dir.h>
29#include <wx/filefn.h>
30#include <wx/textdlg.h>
31#include "creature_sprite_manager.h"
32
33// Initialize static member
34wxLongLong CreaturePalettePanel::last_search_click(0);
35
36// Define the new event ID for the Load NPCs button
37#define PALETTE_LOAD_NPCS_BUTTON 1952
38#define PALETTE_LOAD_MONSTERS_BUTTON 1953
39#define PALETTE_PURGE_CREATURES_BUTTON 1954
40#define PALETTE_SEARCH_BUTTON 1955
41#define PALETTE_SEARCH_FIELD 1956
42#define PALETTE_VIEW_TOGGLE_BUTTON 1957
43#define PALETTE_CREATURE_LARGE_SPRITES_TOGGLE 1958
44#define PALETTE_CREATURE_ZOOM_BUTTON 1959
45
46// ============================================================================
47// Creature palette
48
49BEGIN_EVENT_TABLE(CreaturePalettePanel, PalettePanel)
50EVT_CHOICE(PALETTE_CREATURE_TILESET_CHOICE, CreaturePalettePanel::OnTilesetChange)
51EVT_LISTBOX(PALETTE_CREATURE_LISTBOX, CreaturePalettePanel::OnListBoxChange)
52EVT_COMMAND(wxID_ANY, wxEVT_COMMAND_LISTBOX_SELECTED, CreaturePalettePanel::OnSpriteSelected)
53EVT_TOGGLEBUTTON(PALETTE_CREATURE_BRUSH_BUTTON, CreaturePalettePanel::OnClickCreatureBrushButton)
54EVT_TOGGLEBUTTON(PALETTE_SPAWN_BRUSH_BUTTON, CreaturePalettePanel::OnClickSpawnBrushButton)
55EVT_TOGGLEBUTTON(PALETTE_VIEW_TOGGLE_BUTTON, CreaturePalettePanel::OnClickViewToggle)
56EVT_TOGGLEBUTTON(PALETTE_CREATURE_VIEW_STYLE_TOGGLE, CreaturePalettePanel::OnClickViewStyleToggle)
57EVT_TOGGLEBUTTON(PALETTE_CREATURE_LARGE_SPRITES_TOGGLE, CreaturePalettePanel::OnClickLargeSpritesToggle)
58EVT_BUTTON(PALETTE_CREATURE_ZOOM_BUTTON, CreaturePalettePanel::OnClickZoomButton)
59EVT_BUTTON(PALETTE_LOAD_NPCS_BUTTON, CreaturePalettePanel::OnClickLoadNPCsButton)
60EVT_BUTTON(PALETTE_LOAD_MONSTERS_BUTTON, CreaturePalettePanel::OnClickLoadMonstersButton)
61EVT_BUTTON(PALETTE_PURGE_CREATURES_BUTTON, CreaturePalettePanel::OnClickPurgeCreaturesButton)
62EVT_BUTTON(PALETTE_SEARCH_BUTTON, CreaturePalettePanel::OnClickSearchButton)
63EVT_TEXT(PALETTE_SEARCH_FIELD, CreaturePalettePanel::OnSearchFieldText)
64EVT_SPINCTRL(PALETTE_CREATURE_SPAWN_TIME, CreaturePalettePanel::OnChangeSpawnTime)
65EVT_SPINCTRL(PALETTE_CREATURE_SPAWN_SIZE, CreaturePalettePanel::OnChangeSpawnSize)
66END_EVENT_TABLE()
67
68CreaturePalettePanel::CreaturePalettePanel(wxWindow* parent, wxWindowID id) :
69 PalettePanel(parent, id),
70 tileset_choice(nullptr),
71 creature_list(nullptr),
72 sprite_panel(nullptr),
73 seamless_panel(nullptr),
74 view_toggle(nullptr),
75 view_style_toggle(nullptr),
76 large_sprites_toggle(nullptr),
77 zoom_button(nullptr),
78 view_sizer(nullptr),
79 use_sprite_view(false),
80 use_seamless_view(true), // Seamless is now the default
81 use_large_sprites(false),
82 zoom_factor(1),
83 handling_event(false),
84 search_field(nullptr),
85 search_button(nullptr),
86 load_npcs_button(nullptr),
87 load_monsters_button(nullptr),
88 purge_creatures_button(nullptr),
89 creature_spawntime_spin(nullptr),
90 spawn_size_spin(nullptr),
91 creature_brush_button(nullptr),
92 spawn_brush_button(nullptr) {
93
94 // Create the controls
95 wxSizer* topsizer = newd wxBoxSizer(wxVERTICAL);
96
97 wxSizer* sidesizer = newd wxStaticBoxSizer(wxVERTICAL, this, "Creatures");
98
99 // Tileset choice
100 tileset_choice = newd wxChoice(this, PALETTE_CREATURE_TILESET_CHOICE, wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
101 sidesizer->Add(tileset_choice, 0, wxEXPAND | wxALL, 5);
102
103 // Search field
104 wxBoxSizer* searchSizer = newd wxBoxSizer(wxHORIZONTAL);
105 searchSizer->Add(newd wxStaticText(this, wxID_ANY, "Search:"), 0, wxCENTER | wxLEFT, 5);
106 search_field = newd wxTextCtrl(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxTE_PROCESS_ENTER);
107 searchSizer->Add(search_field, 1, wxCENTER | wxLEFT, 5);
108 search_button = newd wxButton(this, PALETTE_SEARCH_BUTTON, "Go", wxDefaultPosition, wxDefaultSize, wxBU_EXACTFIT);
109 searchSizer->Add(search_button, 0, wxLEFT, 5);
110 sidesizer->Add(searchSizer, 0, wxEXPAND | wxTOP, 5);
111
112 // Connect the focus events to disable hotkeys during typing
113 search_field->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(CreaturePalettePanel::OnSearchFieldFocus), nullptr, this);
114 search_field->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(CreaturePalettePanel::OnSearchFieldKillFocus), nullptr, this);
115 // Connect key down event to handle key presses in the search field
116 search_field->Connect(wxEVT_KEY_DOWN, wxKeyEventHandler(CreaturePalettePanel::OnSearchFieldKeyDown), nullptr, this);
117
118 // Create view container that will hold both list and sprite views
119 view_sizer = newd wxBoxSizer(wxVERTICAL);
120
121 // Create both views
122 creature_list = newd SortableListBox(this, PALETTE_CREATURE_LISTBOX);
123 sprite_panel = newd CreatureSpritePanel(this);
124 seamless_panel = newd CreatureSeamlessGridPanel(this);
125
126 // Add views to sizer (only one will be shown at a time)
127 view_sizer->Add(creature_list, 1, wxEXPAND);
128 view_sizer->Add(sprite_panel, 1, wxEXPAND);
129 view_sizer->Add(seamless_panel, 1, wxEXPAND);
130 sprite_panel->Hide(); // Initially hide the sprite view
131 seamless_panel->Hide(); // Initially hide the seamless view
132
133 sidesizer->Add(view_sizer, 1, wxEXPAND | wxTOP, 5);
134
135 // Add buttons for loading NPCs, monsters, and purging creatures
136 wxSizer* buttonSizer = newd wxBoxSizer(wxHORIZONTAL);
137
138 load_npcs_button = newd wxButton(this, PALETTE_LOAD_NPCS_BUTTON, "Load NPCs Folder");
139 buttonSizer->Add(load_npcs_button, 1, wxEXPAND | wxRIGHT, 5);
140
141 load_monsters_button = newd wxButton(this, PALETTE_LOAD_MONSTERS_BUTTON, "Load Monsters Folder");
142 buttonSizer->Add(load_monsters_button, 1, wxEXPAND | wxLEFT, 5);
143
144 sidesizer->Add(buttonSizer, 0, wxEXPAND | wxTOP, 5);
145
146 purge_creatures_button = newd wxButton(this, PALETTE_PURGE_CREATURES_BUTTON, "Purge Creatures");
147 sidesizer->Add(purge_creatures_button, 0, wxEXPAND | wxTOP, 5);
148
149 // View mode toggle
150 wxBoxSizer* viewModeSizer = newd wxBoxSizer(wxHORIZONTAL);
151 view_toggle = newd wxToggleButton(this, PALETTE_VIEW_TOGGLE_BUTTON, "Sprite View");
152 viewModeSizer->Add(view_toggle, 1, wxEXPAND);
153
154 // Large sprites toggle
155 large_sprites_toggle = newd wxToggleButton(this, PALETTE_CREATURE_LARGE_SPRITES_TOGGLE, "64x64");
156 large_sprites_toggle->Enable(false); // Only enabled in sprite view
157 viewModeSizer->Add(large_sprites_toggle, 1, wxEXPAND | wxLEFT, 5);
158
159 // Zoom button
160 zoom_button = newd wxButton(this, PALETTE_CREATURE_ZOOM_BUTTON, "Zoom 2x");
161 zoom_button->Enable(false); // Only enabled in sprite view with large sprites
162 viewModeSizer->Add(zoom_button, 1, wxEXPAND | wxLEFT, 5);
163
164 sidesizer->Add(viewModeSizer, 0, wxEXPAND | wxTOP, 5);
165
166 // Add brush radio buttons
167 wxToggleButton* creature_radio = newd wxToggleButton(this, PALETTE_CREATURE_BRUSH_BUTTON, "Creature");
168 wxToggleButton* spawn_radio = newd wxToggleButton(this, PALETTE_SPAWN_BRUSH_BUTTON, "Spawn");
169
170 wxBoxSizer* radiosizer = newd wxBoxSizer(wxHORIZONTAL);
171 radiosizer->Add(creature_radio, 1, wxEXPAND);
172 radiosizer->Add(spawn_radio, 1, wxEXPAND);
173
174 sidesizer->Add(radiosizer, 0, wxEXPAND | wxTOP, 5);
175
176 // Store references to the radio buttons
177 creature_brush_button = creature_radio;
178 spawn_brush_button = spawn_radio;
179
180 // Add spawn settings
181 wxFlexGridSizer* settings_sizer = newd wxFlexGridSizer(2, 5, 5);
182 settings_sizer->AddGrowableCol(1);
183 settings_sizer->Add(newd wxStaticText(this, wxID_ANY, "Spawntime"));
184
185 creature_spawntime_spin = newd wxSpinCtrl(this, PALETTE_CREATURE_SPAWN_TIME, i2ws(g_settings.getInteger(Config::DEFAULT_SPAWNTIME)),
186 wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 0, 3600, g_settings.getInteger(Config::DEFAULT_SPAWNTIME));
187
188 settings_sizer->Add(creature_spawntime_spin, 0, wxEXPAND);
189 settings_sizer->Add(newd wxStaticText(this, wxID_ANY, "Size"));
190
191 spawn_size_spin = newd wxSpinCtrl(this, PALETTE_CREATURE_SPAWN_SIZE, i2ws(5),
192 wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, 1, 10, 5);
193
194 settings_sizer->Add(spawn_size_spin, 0, wxEXPAND);
195
196 sidesizer->Add(settings_sizer, 0, wxEXPAND | wxTOP, 5);
197 topsizer->Add(sidesizer, 1, wxEXPAND | wxALL, 5);
198
199 SetSizerAndFit(topsizer);
200
201 // Load all creatures
202 TilesetContainer tilesets;
203
204 // Create a list of all creature tilesets
205 for (TilesetContainer::const_iterator iter = g_materials.tilesets.begin(); iter != g_materials.tilesets.end(); ++iter) {
206 const TilesetCategory* tc = iter->second->getCategory(TILESET_CREATURE);
207 if (tc && tc->size() > 0) {
208 if (tilesets.count(iter->first) == 0)
209 tilesets[iter->first] = iter->second;
210 }
211 }
212
213 // Add them to the choice control
214 for (TilesetContainer::const_iterator iter = tilesets.begin(); iter != tilesets.end(); ++iter) {
215 tileset_choice->Append(wxstr(iter->second->name), const_cast<TilesetCategory*>(iter->second->getCategory(TILESET_CREATURE)));
216 }
217
218 // Add the rest of the tilesets as before
219 for (TilesetContainer::const_iterator iter = g_materials.tilesets.begin(); iter != g_materials.tilesets.end(); ++iter) {
220 if (iter->first == "All Creatures") continue; // Skip since we already added it
221
222 const TilesetCategory* tsc = iter->second->getCategory(TILESET_CREATURE);
223 if (tsc && tsc->size() > 0) {
224 tileset_choice->Append(wxstr(iter->second->name), const_cast<TilesetCategory*>(tsc));
225 } else if (iter->second->name == "NPCs" || iter->second->name == "Others") {
226 Tileset* ts = const_cast<Tileset*>(iter->second);
227 TilesetCategory* rtsc = ts->getCategory(TILESET_CREATURE);
228 tileset_choice->Append(wxstr(ts->name), rtsc);
229 }
230 }
231 SelectTileset(0);
232}
233
234CreaturePalettePanel::~CreaturePalettePanel() {
235 ////
236}
237
238PaletteType CreaturePalettePanel::GetType() const {
239 return TILESET_CREATURE;
240}
241
242void CreaturePalettePanel::SelectFirstBrush() {
243 if (use_sprite_view) {
244 if (use_seamless_view) {
245 // Select first creature in seamless panel
246 if (!seamless_panel->creatures.empty()) {
247 seamless_panel->SelectIndex(0);
248 }
249 } else {
250 // Select first creature in sprite panel
251 if (!sprite_panel->creatures.empty()) {
252 sprite_panel->SelectIndex(0);
253 }
254 }
255 } else {
256 // Select first creature in list
257 if (creature_list->GetCount() > 0) {
258 creature_list->SetSelection(0);
259 }
260 }
261}
262
263Brush* CreaturePalettePanel::GetSelectedBrush() const {
264 if (use_sprite_view) {
265 if (use_seamless_view) {
266 return seamless_panel->GetSelectedBrush();
267 } else {
268 return sprite_panel->GetSelectedBrush();
269 }
270 } else {
271 if (creature_list->GetCount() > 0 && creature_list->GetSelection() != wxNOT_FOUND) {
272 const Brush* brush = reinterpret_cast<const Brush*>(creature_list->GetClientData(creature_list->GetSelection()));
273 if (brush) {
274 if (g_gui.GetCurrentBrush() != brush) {
275 g_gui.SelectBrush(const_cast<Brush*>(brush), TILESET_CREATURE);
276 }
277 }
278 return const_cast<Brush*>(brush);
279 }
280 return nullptr;
281 }
282}
283
284bool CreaturePalettePanel::SelectBrush(const Brush* whatbrush) {
285 if (!whatbrush) {
286 if (use_sprite_view) {
287 if (use_seamless_view) {
288 seamless_panel->SelectBrush(nullptr);
289 } else {
290 sprite_panel->SelectBrush(nullptr);
291 }
292 } else {
293 creature_list->SetSelection(wxNOT_FOUND);
294 }
295 return true;
296 }
297
298 if (whatbrush->isCreature()) {
299 if (use_sprite_view) {
300 if (use_seamless_view) {
301 return seamless_panel->SelectBrush(whatbrush);
302 } else {
303 return sprite_panel->SelectBrush(whatbrush);
304 }
305 } else {
306 for (size_t i = 0; i < creature_list->GetCount(); ++i) {
307 const Brush* tmp_brush = reinterpret_cast<const Brush*>(creature_list->GetClientData(i));
308 if (tmp_brush == whatbrush) {
309 creature_list->SetSelection(i);
310 return true;
311 }
312 }
313 }
314 }
315 return false;
316}
317
318int CreaturePalettePanel::GetSelectedBrushSize() const {
319 return spawn_size_spin->GetValue();
320}
321
322void CreaturePalettePanel::OnUpdate() {
323 tileset_choice->Clear();
324 g_materials.createOtherTileset();
325
326 // Create an "All Creatures" tileset that contains all creatures
327 Tileset* allCreatures = nullptr;
328 TilesetCategory* allCreaturesCategory = nullptr;
329
330 // Check if the "All Creatures" tileset already exists, if not create it
331 if (g_materials.tilesets.count("All Creatures") > 0) {
332 allCreatures = g_materials.tilesets["All Creatures"];
333 allCreaturesCategory = allCreatures->getCategory(TILESET_CREATURE);
334 allCreaturesCategory->brushlist.clear();
335 } else {
336 allCreatures = newd Tileset(g_brushes, "All Creatures");
337 g_materials.tilesets["All Creatures"] = allCreatures;
338 allCreaturesCategory = allCreatures->getCategory(TILESET_CREATURE);
339 }
340
341 // Track added creatures to avoid duplicates
342 std::set<std::string> addedCreatures;
343
344 // Collect all creature brushes from all tilesets
345 for (TilesetContainer::const_iterator iter = g_materials.tilesets.begin(); iter != g_materials.tilesets.end(); ++iter) {
346 if (iter->first == "All Creatures") continue; // Skip ourselves to avoid duplication
347
348 const TilesetCategory* tsc = iter->second->getCategory(TILESET_CREATURE);
349 if (tsc && tsc->size() > 0) {
350 // Add all creature brushes from this category to the All Creatures category
351 for (BrushVector::const_iterator brushIter = tsc->brushlist.begin(); brushIter != tsc->brushlist.end(); ++brushIter) {
352 if ((*brushIter)->isCreature()) {
353 // Only add if not already added (avoid duplicates)
354 std::string creatureName = (*brushIter)->getName();
355 if (addedCreatures.count(creatureName) == 0) {
356 allCreaturesCategory->brushlist.push_back(*brushIter);
357 addedCreatures.insert(creatureName);
358 }
359 }
360 }
361 }
362 }
363
364 // Add the "All Creatures" tileset first
365 tileset_choice->Append(wxstr(allCreatures->name), allCreaturesCategory);
366
367 // Add the rest of the tilesets as before
368 for (TilesetContainer::const_iterator iter = g_materials.tilesets.begin(); iter != g_materials.tilesets.end(); ++iter) {
369 if (iter->first == "All Creatures") continue; // Skip since we already added it
370
371 const TilesetCategory* tsc = iter->second->getCategory(TILESET_CREATURE);
372 if (tsc && tsc->size() > 0) {
373 tileset_choice->Append(wxstr(iter->second->name), const_cast<TilesetCategory*>(tsc));
374 } else if (iter->second->name == "NPCs" || iter->second->name == "Others") {
375 Tileset* ts = const_cast<Tileset*>(iter->second);
376 TilesetCategory* rtsc = ts->getCategory(TILESET_CREATURE);
377 tileset_choice->Append(wxstr(ts->name), rtsc);
378 }
379 }
380 SelectTileset(0);
381}
382
383void CreaturePalettePanel::OnUpdateBrushSize(BrushShape shape, int size) {
384 return spawn_size_spin->SetValue(size);
385}
386
387void CreaturePalettePanel::OnSwitchIn() {
388 g_gui.ActivatePalette(GetParentPalette());
389 g_gui.SetBrushSize(spawn_size_spin->GetValue());
390}
391
392void CreaturePalettePanel::SelectTileset(size_t index) {
393 ASSERT(tileset_choice->GetCount() >= index);
394
395 creature_list->Clear();
396 sprite_panel->Clear();
397 seamless_panel->Clear();
398
399 if (tileset_choice->GetCount() == 0) {
400 // No tilesets :(
401 creature_brush_button->Enable(false);
402 } else {
403 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
404
405 // Add creatures to appropriate view
406 if (use_sprite_view) {
407 sprite_panel->LoadCreatures(tsc->brushlist);
408 } else {
409 // Add creatures to list view
410 for (BrushVector::const_iterator iter = tsc->brushlist.begin();
411 iter != tsc->brushlist.end();
412 ++iter) {
413 // Check if this creature has outfit colors
414 CreatureBrush* cb = dynamic_cast<CreatureBrush*>(*iter);
415 if (cb && cb->getType()) {
416 const Outfit& outfit = cb->getType()->outfit;
417
418 // Create name for list display
419 std::string name = (*iter)->getName();
420
421 // If this creature has custom outfit colors, add an indicator to the name
422 if (outfit.lookHead > 0 || outfit.lookBody > 0 || outfit.lookLegs > 0 || outfit.lookFeet > 0) {
423 name += " [outfit]";
424 }
425
426 creature_list->Append(wxstr(name), *iter);
427 } else {
428 // Regular creature without custom outfit
429 creature_list->Append(wxstr((*iter)->getName()), *iter);
430 }
431 }
432 creature_list->Sort();
433 }
434
435 // Apply filter if search field has text
436 if (!search_field->IsEmpty()) {
437 FilterCreatures(search_field->GetValue());
438 } else {
439 SelectCreature(0);
440 }
441
442 tileset_choice->SetSelection(index);
443 }
444}
445
446void CreaturePalettePanel::SelectCreature(size_t index) {
447 // Select creature by index
448 if (use_sprite_view) {
449 // In sprite view, select by index
450 if (index < sprite_panel->creatures.size()) {
451 sprite_panel->SelectIndex(index);
452 }
453 } else {
454 // In list view, select by index
455 if (creature_list->GetCount() > 0 && index < creature_list->GetCount()) {
456 creature_list->SetSelection(index);
457 }
458 }
459
460 SelectCreatureBrush();
461}
462
463void CreaturePalettePanel::SelectCreature(std::string name) {
464 if (use_sprite_view) {
465 // In sprite view, find and select brush by name
466 for (size_t i = 0; i < sprite_panel->creatures.size(); ++i) {
467 if (sprite_panel->creatures[i]->getName() == name) {
468 sprite_panel->SelectIndex(i);
469 break;
470 }
471 }
472 } else {
473 // In list view, select by name string
474 if (creature_list->GetCount() > 0) {
475 if (!creature_list->SetStringSelection(wxstr(name))) {
476 creature_list->SetSelection(0);
477 }
478 }
479 }
480
481 SelectCreatureBrush();
482}
483
484void CreaturePalettePanel::SelectCreatureBrush() {
485 bool has_selection = false;
486
487 if (use_sprite_view) {
488 has_selection = (sprite_panel->GetSelectedBrush() != nullptr);
489 } else {
490 has_selection = (creature_list->GetCount() > 0);
491 }
492
493 if (has_selection) {
494 creature_brush_button->Enable(true);
495 creature_brush_button->SetValue(true);
496 spawn_brush_button->SetValue(false);
497 } else {
498 creature_brush_button->Enable(false);
499 SelectSpawnBrush();
500 }
501}
502
503void CreaturePalettePanel::SelectSpawnBrush() {
504 // g_gui.house_exit_brush->setHouse(house);
505 creature_brush_button->SetValue(false);
506 spawn_brush_button->SetValue(true);
507}
508
509void CreaturePalettePanel::OnTilesetChange(wxCommandEvent& event) {
510 SelectTileset(event.GetSelection());
511 g_gui.ActivatePalette(GetParentPalette());
512 g_gui.SelectBrush();
513}
514
515void CreaturePalettePanel::OnListBoxChange(wxCommandEvent& event) {
516 // Get the selected brush before updating
517 Brush* old_brush = g_gui.GetCurrentBrush();
518
519 // Update selection
520 SelectCreature(event.GetSelection());
521 g_gui.ActivatePalette(GetParentPalette());
522
523 // Get the newly selected brush
524 Brush* new_brush = g_gui.GetCurrentBrush();
525
526 // If we selected the same brush, first set to nullptr then reselect
527 if(old_brush && new_brush && old_brush == new_brush) {
528 g_gui.SelectBrush(nullptr, TILESET_CREATURE);
529 }
530
531 // Now select the brush (either for the first time or re-selecting)
532 g_gui.SelectBrush();
533}
534
535void CreaturePalettePanel::OnClickCreatureBrushButton(wxCommandEvent& event) {
536 SelectCreatureBrush();
537 g_gui.ActivatePalette(GetParentPalette());
538 g_gui.SelectBrush();
539}
540
541void CreaturePalettePanel::OnClickSpawnBrushButton(wxCommandEvent& event) {
542 SelectSpawnBrush();
543 g_gui.ActivatePalette(GetParentPalette());
544 g_gui.SelectBrush();
545}
546
547void CreaturePalettePanel::OnClickLoadNPCsButton(wxCommandEvent& event) {
548 wxDirDialog dlg(g_gui.root, "Select NPC folder", "", wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
549 if (dlg.ShowModal() == wxID_OK) {
550 wxString folder = dlg.GetPath();
551 LoadNPCsFromFolder(folder);
552 }
553}
554
555void CreaturePalettePanel::OnClickLoadMonstersButton(wxCommandEvent& event) {
556 wxDirDialog dlg(g_gui.root, "Select Monsters folder", "", wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
557 if (dlg.ShowModal() == wxID_OK) {
558 wxString folder = dlg.GetPath();
559 LoadMonstersFromFolder(folder);
560 }
561}
562
563void CreaturePalettePanel::OnClickPurgeCreaturesButton(wxCommandEvent& event) {
564 // Confirmation dialog
565 long response = wxMessageBox("Are you sure you want to purge all creatures from the palette? This cannot be undone.",
566 "Confirm Purge", wxYES_NO | wxICON_QUESTION, g_gui.root);
567
568 if (response == wxYES) {
569 PurgeCreaturePalettes();
570 }
571}
572
573bool CreaturePalettePanel::LoadNPCsFromFolder(const wxString& folder) {
574 // Get all .xml files in the folder
575 wxArrayString files;
576 wxDir::GetAllFiles(folder, &files, "*.xml", wxDIR_FILES);
577
578 if (files.GetCount() == 0) {
579 wxMessageBox("No XML files found in the selected folder.", "Error", wxOK | wxICON_INFORMATION, g_gui.root);
580 return false;
581 }
582
583 wxArrayString warnings;
584 int loadedCount = 0;
585
586 for (size_t i = 0; i < files.GetCount(); ++i) {
587 wxString error;
588 bool ok = g_creatures.importXMLFromOT(FileName(files[i]), error, warnings);
589 if (ok) {
590 loadedCount++;
591 } else {
592 warnings.Add("Failed to load " + files[i] + ": " + error);
593 }
594 }
595
596 if (!warnings.IsEmpty()) {
597 g_gui.ListDialog("NPC loader messages", warnings);
598 }
599
600 if (loadedCount > 0) {
601 g_gui.PopupDialog("Success", wxString::Format("Successfully loaded %d NPC files.", loadedCount), wxOK);
602
603 // Refresh the palette
604 g_gui.RefreshPalettes();
605
606 // Refresh current tileset and creature list
607 OnUpdate();
608
609 return true;
610 } else {
611 wxMessageBox("No NPCs could be loaded from the selected folder.", "Error", wxOK | wxICON_INFORMATION, g_gui.root);
612 return false;
613 }
614}
615
616bool CreaturePalettePanel::LoadMonstersFromFolder(const wxString& folder) {
617 // Get all .xml files in the folder
618 wxArrayString files;
619 wxDir::GetAllFiles(folder, &files, "*.xml", wxDIR_FILES);
620
621 if (files.GetCount() == 0) {
622 wxMessageBox("No XML files found in the selected folder.", "Error", wxOK | wxICON_INFORMATION, g_gui.root);
623 return false;
624 }
625
626 wxArrayString warnings;
627 int loadedCount = 0;
628
629 for (size_t i = 0; i < files.GetCount(); ++i) {
630 wxString error;
631 bool ok = g_creatures.importXMLFromOT(FileName(files[i]), error, warnings);
632 if (ok) {
633 loadedCount++;
634 } else {
635 warnings.Add("Failed to load " + files[i] + ": " + error);
636 }
637 }
638
639 if (!warnings.IsEmpty()) {
640 g_gui.ListDialog("Monster loader messages", warnings);
641 }
642
643 if (loadedCount > 0) {
644 g_gui.PopupDialog("Success", wxString::Format("Successfully loaded %d monster files.", loadedCount), wxOK);
645
646 // Refresh the palette
647 g_gui.RefreshPalettes();
648
649 // Refresh current tileset and creature list
650 OnUpdate();
651
652 return true;
653 } else {
654 wxMessageBox("No monsters could be loaded from the selected folder.", "Error", wxOK | wxICON_INFORMATION, g_gui.root);
655 return false;
656 }
657}
658
659bool CreaturePalettePanel::PurgeCreaturePalettes() {
660 // Track success
661 bool success = false;
662
663 // Create vectors to store brushes that need to be removed
664 std::vector<Brush*> brushesToRemove;
665
666 // Collect creature brushes from the "NPCs", "Others", and "All Creatures" tilesets
667 if (g_materials.tilesets.count("All Creatures") > 0) {
668 Tileset* allCreaturesTileset = g_materials.tilesets["All Creatures"];
669 TilesetCategory* allCreaturesCategory = allCreaturesTileset->getCategory(TILESET_CREATURE);
670 if (allCreaturesCategory) {
671 allCreaturesCategory->brushlist.clear();
672 success = true;
673 }
674 }
675
676 if (g_materials.tilesets.count("NPCs") > 0) {
677 Tileset* npcTileset = g_materials.tilesets["NPCs"];
678 TilesetCategory* npcCategory = npcTileset->getCategory(TILESET_CREATURE);
679 if (npcCategory) {
680 for (BrushVector::iterator it = npcCategory->brushlist.begin(); it != npcCategory->brushlist.end(); ++it) {
681 brushesToRemove.push_back(*it);
682 }
683 npcCategory->brushlist.clear();
684 success = true;
685 }
686 }
687
688 if (g_materials.tilesets.count("Others") > 0) {
689 Tileset* othersTileset = g_materials.tilesets["Others"];
690 TilesetCategory* othersCategory = othersTileset->getCategory(TILESET_CREATURE);
691 if (othersCategory) {
692 for (BrushVector::iterator it = othersCategory->brushlist.begin(); it != othersCategory->brushlist.end(); ++it) {
693 brushesToRemove.push_back(*it);
694 }
695 othersCategory->brushlist.clear();
696 success = true;
697 }
698 }
699
700 // Remove creature brushes from g_brushes
701 // We need to collect the keys to remove first to avoid modifying the map during iteration
702 const BrushMap& allBrushes = g_brushes.getMap();
703 std::vector<std::string> brushKeysToRemove;
704
705 for (BrushMap::const_iterator it = allBrushes.begin(); it != allBrushes.end(); ++it) {
706 if (it->second && it->second->isCreature()) {
707 brushKeysToRemove.push_back(it->first);
708 }
709 }
710
711 // Now remove the brushes from g_brushes
712 for (std::vector<std::string>::iterator it = brushKeysToRemove.begin(); it != brushKeysToRemove.end(); ++it) {
713 g_brushes.removeBrush(*it);
714 }
715
716 // Delete the brush objects to prevent memory leaks
717 for (std::vector<Brush*>::iterator it = brushesToRemove.begin(); it != brushesToRemove.end(); ++it) {
718 delete *it;
719 }
720
721 // Clear creature database
722 g_creatures.clear();
723
724 // Recreate empty tilesets if needed
725 g_materials.createOtherTileset();
726
727 // Refresh the palette
728 g_gui.RefreshPalettes();
729
730 // Refresh current tileset and creature list in this panel
731 OnUpdate();
732
733 if (success) {
734 g_gui.PopupDialog("Success", "All creatures have been purged from the palettes.", wxOK);
735 } else {
736 wxMessageBox("There was a problem purging the creatures.", "Error", wxOK | wxICON_ERROR, g_gui.root);
737 }
738
739 return success;
740}
741
742void CreaturePalettePanel::OnChangeSpawnTime(wxSpinEvent& event) {
743 g_gui.ActivatePalette(GetParentPalette());
744 g_gui.SetSpawnTime(event.GetPosition());
745}
746
747void CreaturePalettePanel::OnChangeSpawnSize(wxSpinEvent& event) {
748 if (!handling_event) {
749 handling_event = true;
750 g_gui.ActivatePalette(GetParentPalette());
751 g_gui.SetBrushSize(event.GetPosition());
752 handling_event = false;
753 }
754}
755
756// Add this as a class member in the header file first
757static wxLongLong last_search_click;
758
759void CreaturePalettePanel::OnClickSearchButton(wxCommandEvent& event) {
760 // Get the text from the search field
761 wxString searchText = search_field->GetValue();
762
763 // Check if this is a double-click (within 500ms)
764 wxLongLong now = wxGetLocalTimeMillis();
765 bool is_double_click = (now - last_search_click) < 500;
766 last_search_click = now;
767
768 // First try to find the creature
769 FilterCreatures(searchText);
770
771 // Show add dialog if:
772 // 1. No results found, OR
773 // 2. Double-clicked Go button
774 if ((use_sprite_view && sprite_panel->creatures.empty()) ||
775 (!use_sprite_view && creature_list->GetCount() == 0) ||
776 is_double_click) {
777
778 // Show dialog to add new creature
779 AddCreatureDialog dialog(this, searchText);
780 if (dialog.ShowModal() == wxID_OK) {
781 // Get the created creature type
782 CreatureType* new_creature = dialog.GetCreatureType();
783 if (new_creature) {
784 // Get the brush that was created in the dialog
785 CreatureBrush* brush = new_creature->brush;
786 if (brush) {
787 // Add to current view directly
788 if (use_sprite_view) {
789 if (use_seamless_view) {
790 seamless_panel->creatures.push_back(brush);
791 seamless_panel->SelectIndex(seamless_panel->creatures.size() - 1);
792 seamless_panel->RecalculateGrid();
793 seamless_panel->Refresh();
794 } else {
795 sprite_panel->creatures.push_back(brush);
796 sprite_panel->SelectIndex(sprite_panel->creatures.size() - 1);
797 sprite_panel->RecalculateGrid();
798 sprite_panel->Refresh();
799 }
800 } else {
801 // Add to list view
802 std::string name = brush->getName();
803 if (new_creature->outfit.lookHead > 0 || new_creature->outfit.lookBody > 0 ||
804 new_creature->outfit.lookLegs > 0 || new_creature->outfit.lookFeet > 0) {
805 name += " [outfit]";
806 }
807 creature_list->Append(wxstr(name), brush);
808 creature_list->SetSelection(creature_list->GetCount() - 1);
809 }
810
811 // Select the brush for use
812 SelectCreatureBrush();
813 g_gui.SelectBrush(brush, TILESET_CREATURE);
814 }
815 }
816 }
817 }
818}
819
820void CreaturePalettePanel::OnSearchFieldText(wxCommandEvent& event) {
821 // Filter as user types
822 FilterCreatures(search_field->GetValue());
823}
824
825void CreaturePalettePanel::OnSearchFieldFocus(wxFocusEvent& event) {
826 // Disable hotkeys when search field receives focus
827 g_gui.DisableHotkeys();
828 event.Skip();
829}
830
831void CreaturePalettePanel::OnSearchFieldKillFocus(wxFocusEvent& event) {
832 // Re-enable hotkeys when search field loses focus
833 g_gui.EnableHotkeys();
834 event.Skip();
835}
836
837void CreaturePalettePanel::OnSearchFieldKeyDown(wxKeyEvent& event) {
838 // Handle Enter key specially
839 if (event.GetKeyCode() == WXK_RETURN) {
840 FilterCreatures(search_field->GetValue());
841 } else if (event.GetKeyCode() == WXK_ESCAPE) {
842 // Clear search field and reset the list on Escape
843 search_field->Clear();
844 FilterCreatures(wxEmptyString);
845 // Set focus back to the map
846 wxWindow* mapCanvas = g_gui.root->FindWindowByName("MapCanvas");
847 if (mapCanvas) {
848 mapCanvas->SetFocus();
849 }
850 } else {
851 // Process the event normally for all other keys
852 event.Skip();
853 }
854}
855
856void CreaturePalettePanel::FilterCreatures(const wxString& search_text) {
857 if (tileset_choice->GetCount() == 0) return;
858
859 // If search is empty, reset to show all creatures
860 if (search_text.IsEmpty()) {
861 int currentSelection = tileset_choice->GetSelection();
862 if (currentSelection != wxNOT_FOUND) {
863 SelectTileset(currentSelection);
864 }
865 return;
866 }
867
868 wxString searchLower = search_text.Lower();
869
870 // Check if we're searching for a specific looktype (format: "lt:123" or "looktype:123")
871 bool isLooktypeSearch = false;
872 int searchLooktype = 0;
873
874 if (searchLower.StartsWith("lt:") || searchLower.StartsWith("looktype:")) {
875 wxString looktypeStr = searchLower.AfterFirst(':');
876 if (looktypeStr.ToInt(&searchLooktype)) {
877 isLooktypeSearch = true;
878 }
879 }
880
881 // Clear current content
882 BrushVector filtered_brushes;
883 std::set<std::string> seenCreatures; // To avoid duplicates in "All Creatures"
884
885 // Get current category
886 int index = tileset_choice->GetSelection();
887 if (index == wxNOT_FOUND) return;
888
889 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
890 bool isAllCreaturesCategory = (tileset_choice->GetString(index) == "All Creatures");
891
892 for (BrushVector::const_iterator iter = tsc->brushlist.begin(); iter != tsc->brushlist.end(); ++iter) {
893 if (!(*iter)->isCreature()) continue;
894
895 CreatureBrush* creatureBrush = dynamic_cast<CreatureBrush*>(*iter);
896 if (!creatureBrush) continue;
897
898 std::string baseCreatureName = (*iter)->getName();
899 wxString name = wxstr(baseCreatureName).Lower();
900
901 // For "All Creatures" category, don't add duplicates
902 if (!isAllCreaturesCategory && seenCreatures.count(baseCreatureName) > 0) {
903 continue;
904 }
905
906 bool match = false;
907
908 // Check if this is a looktype search
909 if (isLooktypeSearch) {
910 // Match by looktype
911 if (creatureBrush->getType() && creatureBrush->getType()->outfit.lookType == searchLooktype) {
912 match = true;
913 }
914 } else {
915 // Standard name search
916 if (name.Find(searchLower) != wxNOT_FOUND) {
917 match = true;
918 }
919 }
920
921 if (match) {
922 filtered_brushes.push_back(*iter);
923 seenCreatures.insert(baseCreatureName);
924 }
925 }
926
927 // Apply the filtered list to the appropriate view
928 if (use_sprite_view) {
929 // Update sprite view
930 sprite_panel->Clear();
931 sprite_panel->LoadCreatures(filtered_brushes);
932 } else {
933 // Update list view
934 creature_list->Clear();
935
936 for (BrushVector::const_iterator iter = filtered_brushes.begin(); iter != filtered_brushes.end(); ++iter) {
937 CreatureBrush* cb = dynamic_cast<CreatureBrush*>(*iter);
938 if (cb && cb->getType()) {
939 const Outfit& outfit = cb->getType()->outfit;
940
941 // Create name for list display
942 std::string name = (*iter)->getName();
943
944 // If this creature has custom outfit colors, add an indicator to the name
945 if (outfit.lookHead > 0 || outfit.lookBody > 0 || outfit.lookLegs > 0 || outfit.lookFeet > 0) {
946 name += " [outfit]";
947 }
948
949 creature_list->Append(wxstr(name), *iter);
950 } else {
951 // Regular creature without custom outfit
952 creature_list->Append(wxstr((*iter)->getName()), *iter);
953 }
954 }
955
956 // Sort the filtered list
957 creature_list->Sort();
958 }
959
960 // Select first result if any
961 if (!filtered_brushes.empty()) {
962 SelectCreature(0);
963 creature_brush_button->Enable(true);
964 } else {
965 creature_brush_button->Enable(false);
966 }
967}
968
969void CreaturePalettePanel::OnSpriteSelected(wxCommandEvent& event) {
970 Brush* old_brush = g_gui.GetCurrentBrush();
971
972 // Update selection
973 SelectCreatureBrush();
974 g_gui.ActivatePalette(GetParentPalette());
975
976 // Get the newly selected brush
977 Brush* new_brush = g_gui.GetCurrentBrush();
978
979 // If we selected the same brush, first set to nullptr then reselect
980 if(old_brush && new_brush && old_brush == new_brush) {
981 g_gui.SelectBrush(nullptr, TILESET_CREATURE);
982 }
983
984 // Now select the brush (either for the first time or re-selecting)
985 g_gui.SelectBrush();
986}
987
988// New method to switch between list and sprite view
989void CreaturePalettePanel::SetViewMode(bool use_sprites) {
990 // Store original selection
991 Brush* selected_brush = GetSelectedBrush();
992
993 // Update mode flag
994 use_sprite_view = use_sprites;
995
996 // Update UI elements
997 view_toggle->SetValue(use_sprites);
998 large_sprites_toggle->Enable(use_sprites); // Only enable large sprites toggle in sprite view mode
999 zoom_button->Enable(use_sprites && use_large_sprites); // Only enable zoom button in large sprite mode
1000
1001 if (use_sprites) {
1002 // Always use seamless view when sprite view is enabled
1003 use_seamless_view = true;
1004
1005 // Switch to sprite view
1006 creature_list->Hide();
1007 sprite_panel->Hide();
1008 seamless_panel->Show();
1009
1010 // Load creatures from the current category
1011 int index = tileset_choice->GetSelection();
1012 if (index != wxNOT_FOUND) {
1013 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
1014
1015 // Determine base cell size
1016 int base_cell_size = use_large_sprites ? 128 : 32;
1017 int cell_size = base_cell_size;
1018
1019 // Apply zoom factor to cell size if in large mode
1020 if (use_large_sprites && zoom_factor > 1) {
1021 cell_size = base_cell_size * zoom_factor;
1022 }
1023
1024 // Pre-generate creature sprites at their natural sizes
1025 g_creature_sprites.clear(); // Clear cache to ensure new sprites are generated
1026
1027 // Also reset sprite dimensions cache in the view panel
1028 if (seamless_panel) {
1029 seamless_panel->sprite_dimensions.clear();
1030 }
1031
1032 // Pre-calculate and store natural sizes for all creatures
1033 for (size_t i = 0; i < tsc->brushlist.size(); ++i) {
1034 Brush* brush = tsc->brushlist[i];
1035 if (brush->isCreature()) {
1036 CreatureBrush* cb = static_cast<CreatureBrush*>(brush);
1037 if (cb && cb->getType()) {
1038 CreatureType* type = cb->getType();
1039
1040 // Calculate natural size for this creature
1041 int natural_size = 32;
1042 if (seamless_panel) {
1043 natural_size = seamless_panel->GetCreatureNaturalSize(type);
1044
1045 // Store in the dimensions map for later use
1046 for (size_t j = 0; j < seamless_panel->creatures.size(); ++j) {
1047 CreatureBrush* panel_cb = static_cast<CreatureBrush*>(seamless_panel->creatures[j]);
1048 if (panel_cb && panel_cb->getType() == type) {
1049 seamless_panel->sprite_dimensions[j] = natural_size;
1050 break;
1051 }
1052 }
1053 }
1054
1055 // Generate sprite at its natural size
1056 const Outfit& outfit = type->outfit;
1057 if (outfit.lookHead || outfit.lookBody || outfit.lookLegs || outfit.lookFeet) {
1058 g_creature_sprites.getSpriteBitmap(outfit.lookType, outfit.lookHead, outfit.lookBody,
1059 outfit.lookLegs, outfit.lookFeet, natural_size, natural_size);
1060 } else {
1061 g_creature_sprites.getSpriteBitmap(outfit.lookType, natural_size, natural_size);
1062 }
1063 }
1064 }
1065 }
1066
1067 // Update the cell size
1068 seamless_panel->sprite_size = cell_size;
1069 seamless_panel->need_full_redraw = true;
1070 seamless_panel->RecalculateGrid();
1071 seamless_panel->LoadCreatures(tsc->brushlist);
1072 }
1073 } else {
1074 // Switch to list view
1075 sprite_panel->Hide();
1076 seamless_panel->Hide();
1077 creature_list->Show();
1078 }
1079
1080 // Update layout
1081 view_sizer->Layout();
1082
1083 // Restore selection
1084 if (selected_brush) {
1085 SelectBrush(selected_brush);
1086 }
1087}
1088
1089void CreaturePalettePanel::OnClickViewToggle(wxCommandEvent& event) {
1090 SetViewMode(view_toggle->GetValue());
1091}
1092
1093void CreaturePalettePanel::OnClickViewStyleToggle(wxCommandEvent& event) {
1094 SetViewStyle(view_style_toggle->GetValue());
1095}
1096
1097// ============================================================================
1098// CreatureSpritePanel - Panel to display creature sprites in a grid
1099
1100BEGIN_EVENT_TABLE(CreatureSpritePanel, wxScrolledWindow)
1101EVT_PAINT(CreatureSpritePanel::OnPaint)
1102EVT_SIZE(CreatureSpritePanel::OnSize)
1103EVT_LEFT_DOWN(CreatureSpritePanel::OnMouseClick)
1104EVT_MOTION(CreatureSpritePanel::OnMouseMove)
1105END_EVENT_TABLE()
1106
1107CreatureSpritePanel::CreatureSpritePanel(wxWindow* parent) :
1108 wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS),
1109 columns(0),
1110 sprite_size(40),
1111 padding(6),
1112 selected_index(-1),
1113 hover_index(-1),
1114 buffer(nullptr) {
1115
1116 // Set background color
1117 SetBackgroundColour(wxColour(245, 245, 245));
1118
1119 // Enable scrolling
1120 SetScrollRate(1, 10);
1121}
1122
1123CreatureSpritePanel::~CreatureSpritePanel() {
1124 delete buffer;
1125}
1126
1127void CreatureSpritePanel::Clear() {
1128 creatures.clear();
1129 selected_index = -1;
1130 hover_index = -1;
1131 Refresh();
1132}
1133
1134void CreatureSpritePanel::LoadCreatures(const BrushVector& brushlist) {
1135 // Clear any existing creatures
1136 creatures.clear();
1137 selected_index = -1;
1138 hover_index = -1;
1139
1140 // Copy valid creature brushes
1141 for (BrushVector::const_iterator iter = brushlist.begin(); iter != brushlist.end(); ++iter) {
1142 if ((*iter)->isCreature()) {
1143 creatures.push_back(*iter);
1144 }
1145 }
1146
1147 // Select first creature if any
1148 if (!creatures.empty()) {
1149 selected_index = 0;
1150 }
1151
1152 // Calculate layout and refresh
1153 RecalculateGrid();
1154 Refresh();
1155}
1156
1157void CreatureSpritePanel::RecalculateGrid() {
1158 // Get the client size of the panel
1159 int panel_width, panel_height;
1160 GetClientSize(&panel_width, &panel_height);
1161
1162 // Calculate number of columns based on available width
1163 columns = std::max(1, (panel_width - padding) / (sprite_size + padding));
1164
1165 // Calculate number of rows
1166 int rows = creatures.empty() ? 0 : (creatures.size() + columns - 1) / columns;
1167
1168 // Set virtual size for scrolling
1169 int virtual_height = rows * (sprite_size + padding) + padding;
1170 SetVirtualSize(panel_width, virtual_height);
1171
1172 // Recreate buffer with new size if needed
1173 if (buffer) {
1174 delete buffer;
1175 buffer = nullptr;
1176 }
1177
1178 if (panel_width > 0 && panel_height > 0) {
1179 buffer = new wxBitmap(panel_width, panel_height);
1180 }
1181}
1182
1183void CreatureSpritePanel::OnPaint(wxPaintEvent& event) {
1184 // Use wxAutoBufferedPaintDC for flicker-free drawing
1185 wxAutoBufferedPaintDC dc(this);
1186 DoPrepareDC(dc);
1187
1188 // Clear background
1189 dc.SetBackground(wxBrush(GetBackgroundColour()));
1190 dc.Clear();
1191
1192 // Get visible region
1193 int x_start, y_start;
1194 GetViewStart(&x_start, &y_start);
1195 int ppuX, ppuY;
1196 GetScrollPixelsPerUnit(&ppuX, &ppuY);
1197 y_start *= ppuY;
1198
1199 int width, height;
1200 GetClientSize(&width, &height);
1201
1202 // Calculate first and last visible row
1203 int first_row = std::max(0, y_start / (sprite_size + padding));
1204 int last_row = std::min((int)((creatures.size() + columns - 1) / columns),
1205 (y_start + height) / (sprite_size + padding) + 1);
1206
1207 // Draw visible sprites
1208 for (int row = first_row; row < last_row; ++row) {
1209 for (int col = 0; col < columns; ++col) {
1210 int index = row * columns + col;
1211 if (index < (int)creatures.size()) {
1212 int x = padding + col * (sprite_size + padding);
1213 int y = padding + row * (sprite_size + padding);
1214
1215 CreatureType* ctype = static_cast<CreatureBrush*>(creatures[index])->getType();
1216 DrawSprite(dc, x, y, ctype, index == selected_index);
1217 }
1218 }
1219 }
1220}
1221
1222void CreatureSpritePanel::DrawSprite(wxDC& dc, int x, int y, CreatureType* ctype, bool selected) {
1223 if (!ctype) return;
1224
1225 // Background
1226 if (selected) {
1227 dc.SetBrush(wxBrush(wxColour(0x80, 0x80, 0x80)));
1228 dc.SetPen(*wxTRANSPARENT_PEN);
1229 dc.DrawRectangle(x, y, sprite_size, sprite_size);
1230 }
1231
1232 // Determine the target sprite size (64x64 for large mode, otherwise same as cell size)
1233 int actual_sprite_size = sprite_size > 64 ? 64 : sprite_size;
1234
1235 // Get or create sprite bitmap at the correct size
1236 wxBitmap* bitmap = nullptr;
1237 if (ctype->outfit.lookType != 0) {
1238 if (ctype->outfit.lookHead || ctype->outfit.lookBody || ctype->outfit.lookLegs || ctype->outfit.lookFeet) {
1239 bitmap = g_creature_sprites.getSpriteBitmap(
1240 ctype->outfit.lookType,
1241 ctype->outfit.lookHead,
1242 ctype->outfit.lookBody,
1243 ctype->outfit.lookLegs,
1244 ctype->outfit.lookFeet,
1245 actual_sprite_size, actual_sprite_size);
1246 } else {
1247 bitmap = g_creature_sprites.getSpriteBitmap(ctype->outfit.lookType, actual_sprite_size, actual_sprite_size);
1248 }
1249
1250 if (bitmap) {
1251 // Calculate position to center the sprite in the cell
1252 int offsetX = (sprite_size - actual_sprite_size) / 2;
1253 int offsetY = (sprite_size - actual_sprite_size) / 2;
1254
1255 // Draw the sprite centered in the cell
1256 dc.DrawBitmap(*bitmap, x + offsetX, y + offsetY, true);
1257 }
1258 }
1259}
1260
1261void CreatureSpritePanel::OnSize(wxSizeEvent& event) {
1262 RecalculateGrid();
1263 Refresh();
1264}
1265
1266void CreatureSpritePanel::OnScroll(wxScrollWinEvent& event) {
1267 Refresh();
1268 event.Skip();
1269}
1270
1271void CreatureSpritePanel::OnMouseClick(wxMouseEvent& event) {
1272 // Get the position in scroll coordinates
1273 int x, y;
1274 CalcUnscrolledPosition(event.GetX(), event.GetY(), &x, &y);
1275
1276 // Find which sprite was clicked
1277 int index = GetSpriteIndexAt(x, y);
1278
1279 if (index >= 0 && index < (int)creatures.size()) {
1280 SelectIndex(index);
1281
1282 // Send selection event to parent
1283 wxCommandEvent selectionEvent(wxEVT_COMMAND_LISTBOX_SELECTED);
1284 selectionEvent.SetEventObject(this);
1285 GetParent()->GetEventHandler()->ProcessEvent(selectionEvent);
1286 }
1287}
1288
1289void CreatureSpritePanel::OnMouseMove(wxMouseEvent& event) {
1290 // Update hover effect if needed
1291 int index = GetSpriteIndexAt(event.GetX(), event.GetY());
1292 if (index != hover_index) {
1293 // Only redraw the cells that changed, not the entire panel
1294 int old_hover = hover_index;
1295 hover_index = index;
1296
1297 // If we had a previous hover, just redraw that cell
1298 if (old_hover >= 0 && old_hover < static_cast<int>(creatures.size())) {
1299 int old_row = old_hover / columns;
1300 int old_col = old_hover % columns;
1301 int x = padding + old_col * (sprite_size + padding);
1302 int y = padding + old_row * (sprite_size + padding);
1303 wxRect oldRect(x, y, sprite_size, sprite_size);
1304 RefreshRect(oldRect, false);
1305 }
1306
1307 // If we have a new hover, just redraw that cell
1308 if (hover_index >= 0 && hover_index < static_cast<int>(creatures.size())) {
1309 int new_row = hover_index / columns;
1310 int new_col = hover_index % columns;
1311 int x = padding + new_col * (sprite_size + padding);
1312 int y = padding + new_row * (sprite_size + padding);
1313 wxRect newRect(x, y, sprite_size, sprite_size);
1314 RefreshRect(newRect, false);
1315 }
1316 }
1317
1318 event.Skip();
1319}
1320
1321int CreatureSpritePanel::GetSpriteIndexAt(int x, int y) const {
1322 // Calculate the column and row
1323 int col = (x - padding) / (sprite_size + padding);
1324 int row = (y - padding) / (sprite_size + padding);
1325
1326 // Check if within sprite bounds
1327 int sprite_x = padding + col * (sprite_size + padding);
1328 int sprite_y = padding + row * (sprite_size + padding);
1329
1330 if (x >= sprite_x && x < sprite_x + sprite_size &&
1331 y >= sprite_y && y < sprite_y + sprite_size) {
1332
1333 // Convert to index
1334 int index = row * columns + col;
1335 if (index >= 0 && index < (int)creatures.size()) {
1336 return index;
1337 }
1338 }
1339
1340 return -1;
1341}
1342
1343void CreatureSpritePanel::SelectIndex(int index) {
1344 if (index >= 0 && index < (int)creatures.size() && index != selected_index) {
1345 selected_index = index;
1346 Refresh();
1347
1348 // Ensure the selected creature is visible
1349 if (selected_index >= 0) {
1350 int row = selected_index / columns;
1351 int col = selected_index % columns;
1352 int x = padding + col * (sprite_size + padding);
1353 int y = padding + row * (sprite_size + padding);
1354
1355 // Scroll to make the selected creature visible
1356 int client_width, client_height;
1357 GetClientSize(&client_width, &client_height);
1358
1359 int x_scroll, y_scroll;
1360 GetViewStart(&x_scroll, &y_scroll);
1361
1362 // Adjust vertical scroll if needed
1363 if (y < y_scroll) {
1364 Scroll(-1, y / 10); // / 10 because of scroll rate
1365 } else if (y + sprite_size > y_scroll + client_height) {
1366 Scroll(-1, (y + sprite_size - client_height) / 10 + 1);
1367 }
1368 }
1369 }
1370}
1371
1372Brush* CreatureSpritePanel::GetSelectedBrush() const {
1373 if (selected_index >= 0 && selected_index < (int)creatures.size()) {
1374 return creatures[selected_index];
1375 }
1376 return nullptr;
1377}
1378
1379bool CreatureSpritePanel::SelectBrush(const Brush* brush) {
1380 if (!brush || !brush->isCreature()) {
1381 return false;
1382 }
1383
1384 for (size_t i = 0; i < creatures.size(); ++i) {
1385 if (creatures[i] == brush) {
1386 SelectIndex(i);
1387 return true;
1388 }
1389 }
1390
1391 return false;
1392}
1393
1394void CreatureSpritePanel::EnsureVisible(const Brush* brush) {
1395 if (!brush || !brush->isCreature()) {
1396 return;
1397 }
1398
1399 for (size_t i = 0; i < creatures.size(); ++i) {
1400 if (creatures[i] == brush) {
1401 // Calculate row of the item
1402 int row = i / columns;
1403 int y = row * sprite_size;
1404
1405 int client_height;
1406 GetClientSize(nullptr, &client_height);
1407
1408 int x_scroll, y_scroll;
1409 GetViewStart(&x_scroll, &y_scroll);
1410
1411 // Adjust vertical scroll if needed
1412 if (y < y_scroll) {
1413 Scroll(-1, y / 10); // / 10 because of scroll rate
1414 }
1415 break;
1416 }
1417 }
1418}
1419
1420int CreatureSpritePanel::GetSpriteSize() const {
1421 return sprite_size;
1422}
1423
1424// Implementation of CreatureSeamlessGridPanel
1425BEGIN_EVENT_TABLE(CreatureSeamlessGridPanel, wxScrolledWindow)
1426EVT_PAINT(CreatureSeamlessGridPanel::OnPaint)
1427EVT_SIZE(CreatureSeamlessGridPanel::OnSize)
1428EVT_LEFT_DOWN(CreatureSeamlessGridPanel::OnMouseClick)
1429EVT_MOTION(CreatureSeamlessGridPanel::OnMouseMove)
1430EVT_SCROLLWIN(CreatureSeamlessGridPanel::OnScroll)
1431EVT_TIMER(wxID_ANY, CreatureSeamlessGridPanel::OnTimer)
1432END_EVENT_TABLE()
1433
1434CreatureSeamlessGridPanel::CreatureSeamlessGridPanel(wxWindow* parent) :
1435 wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxWANTS_CHARS),
1436 columns(1),
1437 sprite_size(32),
1438 selected_index(-1),
1439 hover_index(-1),
1440 buffer(nullptr),
1441 first_visible_row(0),
1442 last_visible_row(0),
1443 visible_rows_margin(10),
1444 total_rows(0),
1445 need_full_redraw(true),
1446 use_progressive_loading(true),
1447 is_large_tileset(false),
1448 loading_step(0),
1449 max_loading_steps(5),
1450 loading_timer(nullptr) {
1451
1452 // Enable background erase to prevent flicker
1453 SetBackgroundStyle(wxBG_STYLE_PAINT);
1454
1455 // Set background color
1456 SetBackgroundColour(wxColour(240, 240, 240));
1457
1458 // Enable scrolling - pixel level for smoother scrolling
1459 SetScrollRate(1, 20);
1460
1461 // Create loading timer for progressive loading
1462 loading_timer = new wxTimer(this);
1463}
1464
1465CreatureSeamlessGridPanel::~CreatureSeamlessGridPanel() {
1466 if (loading_timer) {
1467 loading_timer->Stop();
1468 delete loading_timer;
1469 }
1470 delete buffer;
1471}
1472
1473void CreatureSeamlessGridPanel::Clear() {
1474 creatures.clear();
1475 selected_index = -1;
1476 hover_index = -1;
1477 Refresh();
1478}
1479
1480void CreatureSeamlessGridPanel::LoadCreatures(const BrushVector& brushlist) {
1481 // Clear any existing creatures
1482 creatures.clear();
1483 selected_index = -1;
1484 hover_index = -1;
1485
1486 // Copy valid creature brushes
1487 for (BrushVector::const_iterator iter = brushlist.begin(); iter != brushlist.end(); ++iter) {
1488 if ((*iter)->isCreature()) {
1489 creatures.push_back(*iter);
1490 }
1491 }
1492
1493 // Select first creature if any
1494 if (!creatures.empty()) {
1495 selected_index = 0;
1496 }
1497
1498 // Store natural dimensions for each creature to use when drawing
1499 // This prevents constant recalculation
1500 sprite_dimensions.clear();
1501 for (size_t i = 0; i < creatures.size(); ++i) {
1502 CreatureBrush* cb = static_cast<CreatureBrush*>(creatures[i]);
1503 if (cb && cb->getType()) {
1504 // Determine natural size based on looktype
1505 int natural_size = GetCreatureNaturalSize(cb->getType());
1506 sprite_dimensions[i] = natural_size;
1507 }
1508 }
1509
1510 // Calculate layout and refresh
1511 RecalculateGrid();
1512 Refresh();
1513}
1514
1515void CreatureSeamlessGridPanel::StartProgressiveLoading() {
1516 if (!loading_timer) return;
1517
1518 // Reset loading step
1519 loading_step = 0;
1520
1521 // Set initial small margin for quick initial display
1522 visible_rows_margin = 3;
1523
1524 // Force full redraw
1525 need_full_redraw = true;
1526
1527 // Start timer for progressive loading
1528 loading_timer->Start(150); // 150ms interval for smooth loading
1529
1530 // Force initial redraw to show progress
1531 Refresh();
1532}
1533
1534void CreatureSeamlessGridPanel::OnTimer(wxTimerEvent& event) {
1535 // Progressively increase the loading step
1536 loading_step++;
1537
1538 // Update viewable items with new margin
1539 UpdateViewableItems();
1540
1541 // Force redraw to update progress
1542 Refresh();
1543
1544 // Stop timer when we've reached max loading steps
1545 if (loading_step >= max_loading_steps) {
1546 loading_timer->Stop();
1547 visible_rows_margin = 20; // Set to higher value for regular scrolling
1548 need_full_redraw = true;
1549 Refresh();
1550 }
1551}
1552
1553void CreatureSeamlessGridPanel::RecalculateGrid() {
1554 // Get the client size of the panel
1555 int panel_width, panel_height;
1556 GetClientSize(&panel_width, &panel_height);
1557
1558 // Calculate number of columns based on available width
1559 columns = std::max(1, panel_width / sprite_size);
1560
1561 // Calculate number of rows
1562 total_rows = creatures.empty() ? 0 : (creatures.size() + columns - 1) / columns;
1563
1564 // Set virtual size for scrolling
1565 int virtual_height = total_rows * sprite_size;
1566 SetVirtualSize(panel_width, virtual_height);
1567
1568 // Recreate buffer with new size if needed
1569 if (buffer) {
1570 delete buffer;
1571 buffer = nullptr;
1572 }
1573
1574 if (panel_width > 0 && panel_height > 0) {
1575 buffer = new wxBitmap(panel_width, panel_height);
1576 }
1577
1578 // Update viewable items
1579 UpdateViewableItems();
1580}
1581
1582void CreatureSeamlessGridPanel::UpdateViewableItems() {
1583 int xStart, yStart;
1584 GetViewStart(&xStart, &yStart);
1585 int ppuX, ppuY;
1586 GetScrollPixelsPerUnit(&ppuX, &ppuY);
1587 yStart *= ppuY;
1588
1589 int width, height;
1590 GetClientSize(&width, &height);
1591
1592 // Calculate visible range with margins
1593 int new_first_row = std::max(0, (yStart / sprite_size) - visible_rows_margin);
1594 int new_last_row = std::min(total_rows - 1, ((yStart + height) / sprite_size) + visible_rows_margin);
1595
1596 // Only trigger redraw if visible range changes
1597 if (new_first_row != first_visible_row || new_last_row != last_visible_row) {
1598 first_visible_row = new_first_row;
1599 last_visible_row = new_last_row;
1600 Refresh();
1601 }
1602}
1603
1604void CreatureSeamlessGridPanel::OnScroll(wxScrollWinEvent& event) {
1605 // Handle scroll events to update visible items
1606 UpdateViewableItems();
1607 event.Skip();
1608}
1609
1610void CreatureSeamlessGridPanel::DrawItemsToPanel(wxDC& dc) {
1611 if (creatures.empty()) return;
1612
1613 // Get client area size
1614 int width, height;
1615 GetClientSize(&width, &height);
1616
1617 // Draw loading progress for large datasets during initial load
1618 if (loading_step < max_loading_steps && is_large_tileset) {
1619 // Show loading progress
1620 wxString loadingMessage = wxString::Format("Loading creatures... %d%%",
1621 (loading_step * 100) / max_loading_steps);
1622
1623 // Gray semi-transparent overlay with progress message
1624 dc.SetFont(wxFont(12, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
1625 dc.SetTextForeground(wxColour(50, 50, 50));
1626 dc.DrawLabel(loadingMessage, wxRect(0, 0, width, height), wxALIGN_CENTER);
1627 }
1628
1629 // Draw visible sprites in grid
1630 for (int row = first_visible_row; row <= last_visible_row; ++row) {
1631 for (int col = 0; col < columns; ++col) {
1632 int index = row * columns + col;
1633 if (index < static_cast<int>(creatures.size())) {
1634 int x = col * sprite_size;
1635 int y = row * sprite_size;
1636
1637 CreatureBrush* cb = static_cast<CreatureBrush*>(creatures[index]);
1638 if (cb && cb->getType()) {
1639 DrawCreature(dc, x, y, cb->getType(), index == selected_index);
1640 }
1641 }
1642 }
1643 }
1644}
1645
1646void CreatureSeamlessGridPanel::OnPaint(wxPaintEvent& event) {
1647 wxAutoBufferedPaintDC dc(this);
1648 DoPrepareDC(dc); // For correct scrolling
1649
1650 // Clear background
1651 dc.SetBackground(wxBrush(GetBackgroundColour()));
1652 dc.Clear();
1653
1654 // Draw items
1655 DrawItemsToPanel(dc);
1656}
1657
1658void CreatureSeamlessGridPanel::DrawCreature(wxDC& dc, int x, int y, CreatureType* ctype, bool selected) {
1659 if (!ctype) return;
1660
1661 // Draw selection highlight
1662 if (selected) {
1663 dc.SetBrush(wxBrush(wxColour(0x80, 0x80, 0xFF, 0x80)));
1664 dc.SetPen(wxPen(wxColour(0x80, 0x80, 0xFF), 1));
1665 dc.DrawRectangle(x, y, sprite_size, sprite_size);
1666 }
1667
1668 // For hover effect
1669 if (!selected && selected_index != -1 && hover_index != -1 && hover_index != selected_index) {
1670 int hover_col = hover_index % columns;
1671 int hover_row = hover_index / columns;
1672 int hover_x = hover_col * sprite_size;
1673 int hover_y = hover_row * sprite_size;
1674
1675 if (hover_x == x && hover_y == y) {
1676 dc.SetBrush(wxBrush(wxColour(0xC0, 0xC0, 0xC0, 0x80)));
1677 dc.SetPen(*wxTRANSPARENT_PEN);
1678 dc.DrawRectangle(x, y, sprite_size, sprite_size);
1679 }
1680 }
1681
1682 // Find the natural size of this creature
1683 int index = -1;
1684 for (size_t i = 0; i < creatures.size(); ++i) {
1685 CreatureBrush* cb = static_cast<CreatureBrush*>(creatures[i]);
1686 if (cb && cb->getType() == ctype) {
1687 index = i;
1688 break;
1689 }
1690 }
1691
1692 // Get the natural size from the dimensions map if available
1693 int natural_size = 32;
1694 if (index >= 0 && sprite_dimensions.count(index) > 0) {
1695 natural_size = sprite_dimensions[index];
1696 } else {
1697 // Calculate natural size if not found in the map
1698 natural_size = GetCreatureNaturalSize(ctype);
1699 }
1700
1701 // For zoomed view, determine the display size based on the cell size
1702 int display_size = natural_size;
1703
1704 // Apply scaling based on cell size
1705 if (sprite_size < natural_size) {
1706 // If the cell is smaller than the natural size, scale down
1707 display_size = sprite_size;
1708 } else if (sprite_size > natural_size * 2) {
1709 // If the cell is more than twice the natural size, scale up by a factor
1710 int zoom_factor = sprite_size / natural_size;
1711 // Limit to reasonable zoom
1712 zoom_factor = std::min(zoom_factor, 4);
1713 display_size = natural_size * zoom_factor;
1714 }
1715
1716 // Get or create sprite bitmap at the natural size
1717 wxBitmap* bitmap = nullptr;
1718
1719 if (ctype->outfit.lookType != 0) {
1720 if (ctype->outfit.lookHead || ctype->outfit.lookBody || ctype->outfit.lookLegs || ctype->outfit.lookFeet) {
1721 bitmap = g_creature_sprites.getSpriteBitmap(
1722 ctype->outfit.lookType,
1723 ctype->outfit.lookHead,
1724 ctype->outfit.lookBody,
1725 ctype->outfit.lookLegs,
1726 ctype->outfit.lookFeet,
1727 natural_size, natural_size);
1728 } else {
1729 bitmap = g_creature_sprites.getSpriteBitmap(ctype->outfit.lookType, natural_size, natural_size);
1730 }
1731
1732 if (bitmap) {
1733 // Calculate position to center the sprite in the grid cell
1734 int offsetX = (sprite_size - display_size) / 2;
1735 int offsetY = (sprite_size - display_size) / 2;
1736
1737 // Ensure offsets are not negative
1738 offsetX = std::max(0, offsetX);
1739 offsetY = std::max(0, offsetY);
1740
1741 // Scale the sprite if needed
1742 if (display_size != bitmap->GetWidth() || display_size != bitmap->GetHeight()) {
1743 // Create a temporary scaled bitmap
1744 wxImage original = bitmap->ConvertToImage();
1745 wxBitmap scaled(original.Scale(display_size, display_size, wxIMAGE_QUALITY_HIGH));
1746
1747 // Draw the scaled bitmap
1748 dc.DrawBitmap(scaled, x + offsetX, y + offsetY, true);
1749 } else {
1750 // Draw the original bitmap
1751 dc.DrawBitmap(*bitmap, x + offsetX, y + offsetY, true);
1752 }
1753 }
1754 }
1755
1756 // Draw name label below the sprite
1757 wxString name = wxString(ctype->name.c_str(), wxConvUTF8);
1758 if (!name.IsEmpty()) {
1759 // Set font size based on cell size
1760 int font_size = std::min(10, sprite_size / 12);
1761 font_size = std::max(7, font_size); // Make sure it's not too small
1762
1763 wxFont font(font_size, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
1764 dc.SetFont(font);
1765 dc.SetTextForeground(selected ? wxColour(50, 50, 120) : wxColour(80, 80, 80));
1766
1767 // Position text at the bottom of the cell
1768 int text_y = y + sprite_size - font_size - 4;
1769 wxCoord text_width, text_height;
1770 dc.GetTextExtent(name, &text_width, &text_height);
1771
1772 // Center the text and make sure it fits in the cell
1773 if (text_width > sprite_size - 4) {
1774 // Truncate the text
1775 wxString truncated_name;
1776 int chars_that_fit = 0;
1777 wxArrayInt partial_extents;
1778 dc.GetPartialTextExtents(name, partial_extents);
1779
1780 for (size_t i = 0; i < name.Length(); ++i) {
1781 if (i < partial_extents.GetCount() && partial_extents[i] < sprite_size - 10) {
1782 chars_that_fit = i + 1;
1783 } else {
1784 break;
1785 }
1786 }
1787
1788 if (chars_that_fit > 0) {
1789 truncated_name = name.Left(chars_that_fit) + "...";
1790 dc.DrawText(truncated_name, x + (sprite_size - dc.GetTextExtent(truncated_name).GetWidth()) / 2, text_y);
1791 }
1792 } else {
1793 // Text fits, center it
1794 dc.DrawText(name, x + (sprite_size - text_width) / 2, text_y);
1795 }
1796 }
1797}
1798
1799void CreatureSeamlessGridPanel::OnSize(wxSizeEvent& event) {
1800 RecalculateGrid();
1801 event.Skip();
1802}
1803
1804int CreatureSeamlessGridPanel::GetSpriteIndexAt(int x, int y) const {
1805 // Convert mouse position to logical position (accounting for scrolling)
1806 int logX, logY;
1807 CalcUnscrolledPosition(x, y, &logX, &logY);
1808
1809 // Calculate row and column
1810 int col = logX / sprite_size;
1811 int row = logY / sprite_size;
1812
1813 // Calculate index
1814 int index = row * columns + col;
1815
1816 // Check if this is a valid index
1817 if (index >= 0 && index < static_cast<int>(creatures.size()) &&
1818 col >= 0 && col < columns) {
1819 return index;
1820 }
1821
1822 return -1;
1823}
1824
1825void CreatureSeamlessGridPanel::OnMouseClick(wxMouseEvent& event) {
1826 int index = GetSpriteIndexAt(event.GetX(), event.GetY());
1827 if (index != -1) {
1828 selected_index = index;
1829 Refresh();
1830
1831 // Notify parent of selection
1832 wxCommandEvent selectionEvent(wxEVT_COMMAND_LISTBOX_SELECTED);
1833 wxPostEvent(GetParent(), selectionEvent);
1834 }
1835
1836 event.Skip();
1837}
1838
1839void CreatureSeamlessGridPanel::OnMouseMove(wxMouseEvent& event) {
1840 // Update hover effect if needed
1841 int index = GetSpriteIndexAt(event.GetX(), event.GetY());
1842 if (index != hover_index) {
1843 // Only redraw the cells that changed, not the entire panel
1844 int old_hover = hover_index;
1845 hover_index = index;
1846
1847 // If we had a previous hover, just redraw that cell
1848 if (old_hover >= 0 && old_hover < static_cast<int>(creatures.size())) {
1849 int old_row = old_hover / columns;
1850 int old_col = old_hover % columns;
1851 wxRect oldRect(old_col * sprite_size, old_row * sprite_size, sprite_size, sprite_size);
1852 RefreshRect(oldRect, false);
1853 }
1854
1855 // If we have a new hover, just redraw that cell
1856 if (hover_index >= 0 && hover_index < static_cast<int>(creatures.size())) {
1857 int new_row = hover_index / columns;
1858 int new_col = hover_index % columns;
1859 wxRect newRect(new_col * sprite_size, new_row * sprite_size, sprite_size, sprite_size);
1860 RefreshRect(newRect, false);
1861 }
1862 }
1863
1864 event.Skip();
1865}
1866
1867Brush* CreatureSeamlessGridPanel::GetSelectedBrush() const {
1868 if (selected_index >= 0 && selected_index < static_cast<int>(creatures.size())) {
1869 return creatures[selected_index];
1870 }
1871 return nullptr;
1872}
1873
1874bool CreatureSeamlessGridPanel::SelectBrush(const Brush* whatbrush) {
1875 if (!whatbrush) return false;
1876
1877 for (size_t i = 0; i < creatures.size(); ++i) {
1878 if (creatures[i] == whatbrush) {
1879 SelectIndex(i);
1880 return true;
1881 }
1882 }
1883 return false;
1884}
1885
1886void CreatureSeamlessGridPanel::SelectIndex(int index) {
1887 if (index >= 0 && index < static_cast<int>(creatures.size())) {
1888 // Store the old selection
1889 int old_selection = selected_index;
1890 selected_index = index;
1891
1892 // Only redraw if selection changed
1893 if (old_selection != selected_index) {
1894 Refresh();
1895 }
1896
1897 // Ensure the selected item is visible
1898 EnsureVisible(creatures[index]);
1899 }
1900}
1901
1902void CreatureSeamlessGridPanel::EnsureVisible(const Brush* brush) {
1903 for (size_t i = 0; i < creatures.size(); ++i) {
1904 if (creatures[i] == brush) {
1905 // Calculate row of the item
1906 int row = i / columns;
1907 int y = row * sprite_size;
1908
1909 // Get the visible area
1910 int xStart, yStart;
1911 GetViewStart(&xStart, &yStart);
1912 int ppuX, ppuY;
1913 GetScrollPixelsPerUnit(&ppuX, &ppuY);
1914 yStart *= ppuY;
1915
1916 int clientHeight;
1917 GetClientSize(nullptr, &clientHeight);
1918
1919 // Scroll if necessary
1920 if (y < yStart) {
1921 Scroll(-1, y / ppuY);
1922 } else if (y + sprite_size > yStart + clientHeight) {
1923 Scroll(-1, (y - clientHeight + sprite_size) / ppuY);
1924 }
1925
1926 // Update which items are visible after scrolling
1927 UpdateViewableItems();
1928 break;
1929 }
1930 }
1931}
1932
1933// Update CreaturePalettePanel constructor
1934void CreaturePalettePanel::SetViewStyle(bool use_seamless) {
1935 // Store original selection
1936 Brush* selected_brush = GetSelectedBrush();
1937
1938 // Update mode flag
1939 use_seamless_view = use_seamless;
1940
1941 // Update UI elements
1942 if (use_sprite_view) {
1943 if (use_seamless_view) {
1944 // Switch to seamless grid view
1945 sprite_panel->Hide();
1946 seamless_panel->Show();
1947
1948 // Load creatures from the current category
1949 int index = tileset_choice->GetSelection();
1950 if (index != wxNOT_FOUND) {
1951 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
1952 // Pre-generate creature sprites
1953 int sprite_size = seamless_panel->GetSpriteSize();
1954 g_creature_sprites.generateCreatureSprites(tsc->brushlist, sprite_size, sprite_size);
1955 seamless_panel->LoadCreatures(tsc->brushlist);
1956 }
1957 } else {
1958 // Switch to regular grid view
1959 seamless_panel->Hide();
1960 sprite_panel->Show();
1961
1962 // Load creatures from the current category
1963 int index = tileset_choice->GetSelection();
1964 if (index != wxNOT_FOUND) {
1965 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
1966 // Pre-generate creature sprites
1967 int sprite_size = sprite_panel->GetSpriteSize();
1968 g_creature_sprites.generateCreatureSprites(tsc->brushlist, sprite_size, sprite_size);
1969 sprite_panel->LoadCreatures(tsc->brushlist);
1970 }
1971 }
1972
1973 // Update layout
1974 view_sizer->Layout();
1975 }
1976
1977 // Restore selection
1978 if (selected_brush) {
1979 SelectBrush(selected_brush);
1980 }
1981}
1982
1983void CreaturePalettePanel::SetLargeSpriteMode(bool use_large) {
1984 if (use_large_sprites != use_large) {
1985 use_large_sprites = use_large;
1986 large_sprites_toggle->SetValue(use_large);
1987
1988 // Update zoom button state - only enable when in large sprite mode
1989 zoom_button->Enable(use_large);
1990
1991 // Reset zoom factor when changing sprite size mode
1992 if (!use_large) {
1993 zoom_factor = 1;
1994 zoom_button->SetLabel("Zoom 2x");
1995 }
1996
1997 // Store the currently selected brush
1998 Brush* old_brush = GetSelectedBrush();
1999
2000 // Get current tileset
2001 int index = tileset_choice->GetSelection();
2002 if (index != wxNOT_FOUND) {
2003 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
2004
2005 // Determine base sprite size and cell size
2006 int base_sprite_size = use_large ? 64 : 32;
2007 int base_cell_size = use_large ? 128 : 32;
2008
2009 // Apply zoom factor to cell size if needed
2010 int cell_size = base_cell_size;
2011 if (use_large && zoom_factor > 1) {
2012 cell_size = base_cell_size * zoom_factor;
2013 }
2014
2015 // Force regeneration of creature sprites with new size
2016 g_creature_sprites.clear(); // Clear cache to ensure new sprites are generated
2017 g_creature_sprites.generateCreatureSprites(tsc->brushlist, base_sprite_size, base_sprite_size);
2018
2019 // Update panel settings
2020 if (use_seamless_view) {
2021 // Update seamless panel - use cell_size for grid cell dimensions
2022 seamless_panel->sprite_size = cell_size;
2023 seamless_panel->need_full_redraw = true;
2024 seamless_panel->RecalculateGrid();
2025 seamless_panel->Refresh();
2026 } else {
2027 // Update regular panel - use cell_size for grid cell dimensions
2028 sprite_panel->sprite_size = cell_size;
2029 sprite_panel->RecalculateGrid();
2030 sprite_panel->Refresh();
2031 }
2032
2033 // Reselect the brush that was selected before
2034 if (old_brush) {
2035 SelectBrush(old_brush);
2036 }
2037 }
2038 }
2039}
2040
2041void CreaturePalettePanel::SetZoomLevel(int new_zoom_factor) {
2042 if (zoom_factor != new_zoom_factor) {
2043 zoom_factor = new_zoom_factor;
2044
2045 // Update button label
2046 zoom_button->SetLabel(wxString::Format("Zoom %dx", new_zoom_factor));
2047
2048 // Only apply zoom when in large sprite mode
2049 if (use_large_sprites) {
2050 // Store the currently selected brush
2051 Brush* old_brush = GetSelectedBrush();
2052
2053 // Get current tileset
2054 int index = tileset_choice->GetSelection();
2055 if (index != wxNOT_FOUND) {
2056 const TilesetCategory* tsc = reinterpret_cast<const TilesetCategory*>(tileset_choice->GetClientData(index));
2057
2058 // Base cell size is 128x128 in large mode
2059 int base_cell_size = 128;
2060 int cell_size = base_cell_size * zoom_factor;
2061
2062 // Pre-generate creature sprites at their natural sizes
2063 g_creature_sprites.clear(); // Clear cache to ensure new sprites are generated
2064
2065 // Also reset sprite dimensions cache in the view panel
2066 if (seamless_panel) {
2067 seamless_panel->sprite_dimensions.clear();
2068 }
2069
2070 // Pre-generate sprites at their natural sizes
2071 for (Brush* brush : tsc->brushlist) {
2072 if (brush->isCreature()) {
2073 CreatureBrush* cb = static_cast<CreatureBrush*>(brush);
2074 if (cb && cb->getType()) {
2075 CreatureType* type = cb->getType();
2076
2077 // Get natural size
2078 int natural_size = 32;
2079 if (seamless_panel) {
2080 natural_size = seamless_panel->GetCreatureNaturalSize(type);
2081 }
2082
2083 // Generate sprite at its natural size
2084 const Outfit& outfit = type->outfit;
2085 if (outfit.lookHead || outfit.lookBody || outfit.lookLegs || outfit.lookFeet) {
2086 g_creature_sprites.getSpriteBitmap(outfit.lookType, outfit.lookHead, outfit.lookBody,
2087 outfit.lookLegs, outfit.lookFeet, natural_size, natural_size);
2088 } else {
2089 g_creature_sprites.getSpriteBitmap(outfit.lookType, natural_size, natural_size);
2090 }
2091 }
2092 }
2093 }
2094
2095 // Update panel settings
2096 if (use_seamless_view) {
2097 // Update seamless panel
2098 seamless_panel->sprite_size = cell_size;
2099 seamless_panel->need_full_redraw = true;
2100 seamless_panel->RecalculateGrid();
2101 seamless_panel->Refresh();
2102 } else {
2103 // Update regular panel
2104 sprite_panel->sprite_size = cell_size;
2105 sprite_panel->RecalculateGrid();
2106 sprite_panel->Refresh();
2107 }
2108
2109 // Reselect the brush that was selected before
2110 if (old_brush) {
2111 SelectBrush(old_brush);
2112 }
2113 }
2114 }
2115 }
2116}
2117
2118void CreaturePalettePanel::OnClickZoomButton(wxCommandEvent& event) {
2119 // Toggle between zoom levels (1x, 2x, 3x, back to 1x)
2120 int new_zoom_factor = (zoom_factor % 3) + 1;
2121 SetZoomLevel(new_zoom_factor);
2122}
2123
2124void CreaturePalettePanel::OnClickLargeSpritesToggle(wxCommandEvent& event) {
2125 SetLargeSpriteMode(event.IsChecked());
2126}
2127
2128int CreatureSeamlessGridPanel::GetCreatureNaturalSize(CreatureType* ctype) const {
2129 if (!ctype) return 32;
2130
2131 // Get sprite from graphics system to check dimensions
2132 GameSprite* spr = g_gui.gfx.getCreatureSprite(ctype->outfit.lookType);
2133 if (!spr) return 32;
2134
2135 // Get natural dimensions from sprite
2136 int natural_width = spr->width > 0 ? spr->width : 32;
2137 int natural_height = spr->height > 0 ? spr->height : 32;
2138
2139 // Calculate natural size as the maximum of width and height
2140 int natural_size = std::max(natural_width, natural_height);
2141
2142 // Round up to nearest standard size (32, 64, 96, 128)
2143 if (natural_size <= 32) {
2144 natural_size = 32;
2145 } else if (natural_size <= 64) {
2146 natural_size = 64;
2147 } else if (natural_size <= 96) {
2148 natural_size = 96;
2149 } else if (natural_size <= 128) {
2150 natural_size = 128;
2151 } else {
2152 natural_size = ((natural_size + 31) / 32) * 32; // Round up to nearest multiple of 32
2153 }
2154
2155 // Fallback based on looktype for sprites without proper dimensions
2156 if (natural_size == 32 && ctype->outfit.lookType >= 800) {
2157 natural_size = 64; // Many higher looktype monsters are larger
2158 }
2159
2160 // Fallback for very high looktypes
2161 if (ctype->outfit.lookType >= 1200 && natural_size < 96) {
2162 natural_size = 96; // Some newer monsters can be much larger
2163 }
2164
2165 return natural_size;
2166}
2167
Top