From d4017f2e3a2270e31912d0b12bf5d6a1b1353e6d Mon Sep 17 00:00:00 2001 From: Espyo Date: Sun, 29 Dec 2024 00:17:07 +0000 Subject: [PATCH] Many more improvements to the particle editor. - General cleanup. - Added common content properties. - The generator can now be moved with the mouse on the canvas, and can be rotated in the properties. - Pausing/Playing now exists for the particle manager and for the generator separately. - Better uniform distribution for rectangular shapes. - Updated the manual. --- manual/content/particle.html | 266 ++++++----------- source/documents/todo.txt | 21 +- .../game_states/animation_editor/drawing.cpp | 2 +- .../game_states/animation_editor/editor.h | 3 + .../game_states/animation_editor/gui.cpp | 42 ++- .../game_states/area_editor/drawing.cpp | 2 +- .../source/game_states/area_editor/editor.h | 4 +- source/source/game_states/area_editor/gui.cpp | 6 +- source/source/game_states/editor.cpp | 32 +- source/source/game_states/editor.h | 6 + .../source/game_states/gui_editor/drawing.cpp | 2 +- source/source/game_states/gui_editor/gui.cpp | 2 +- .../game_states/particle_editor/drawing.cpp | 158 +++++----- .../game_states/particle_editor/editor.cpp | 172 +++++++---- .../game_states/particle_editor/editor.h | 57 +++- .../particle_editor/event_handling.cpp | 28 +- .../game_states/particle_editor/gui.cpp | 257 ++++++++++++++-- source/source/particle.cpp | 280 ++++++++++++------ source/source/particle.h | 2 +- source/source/utils/geometry_utils.cpp | 101 +++++++ source/source/utils/geometry_utils.h | 7 + source/source/utils/imgui_utils.cpp | 11 +- source/source/utils/imgui_utils.h | 1 + source/source/utils/math_utils.cpp | 21 ++ source/source/utils/math_utils.h | 9 +- 25 files changed, 1001 insertions(+), 491 deletions(-) diff --git a/manual/content/particle.html b/manual/content/particle.html index 1ec9a6ddd..cb61e4376 100644 --- a/manual/content/particle.html +++ b/manual/content/particle.html @@ -14,185 +14,103 @@
-

This page will guide you on how to make a particle generator for Pikifen. These are pieces of content so you should know how content works in the engine in general before starting. A particle generator is a set of information about how the engine should throw particles into the game world. Particles are small images that don't interact with the environment, like rocks flying around, clouds of gas floating by, or even the trail left behind by thrown Pikmin and leaders. Then, in-game, enemies, or obstacles, or what have you can call one of these generators in order to generate particles.

- -

Introduction

+

This page will guide you on how to make a particle generator for Pikifen.

-

A generator can tell the engine to generate one single particle, or multiple ones, and instantly or over any amount of time. For instance, if an enemy wants to spit a puff of gas, it should generate 10 or so clouds of gas, all at once, and that's the end of it. But if an obstacle wants to continuously spit out thick smoke, it would generate gray clouds over time; it would likely spit a bunch of them every half second or so, to keep a constant stream. So the logic depends on what you want to use the generator for.

- -

Particles are static images, but they can move, change size, and change opacity over time, and can also be tinted with a certain color. Each generator can only emit one type of particle, but their properties (size, position, etc.) are randomly decided every time one is created, according to the generator's settings. This decision is based on the deviation properties. For instance, when a particle of size 32 is created, if the size deviation property is set to 3, that means the particle can spawn with 3 fewer pixels in size, 3 more pixels in size, or anything in between. 0, for any deviation property, means no deviation, naturally.

- -

The folders that have a list of all particle generators are game_data/<pack>/particle_generators. These contain all custom-made particle generators but do not contain particle generators hardcoded inside the engine's logic itself, for internal use. Each one per data file in the folders is a generator, with the file's name representing the generator's internal name.

- -

Base particle

+

Key concepts

+ +
+
Particle generators are pieces of content.
+
You should know how content works in the engine in general before starting. The folders that have a list of all particle generators can be found in game_data/<pack>/particle_generators.
+
Particle generators contain information on how particles are generated.
+
Each one holds all the necessary information about how the engine should spawn and move particles in the game world. Then, in-game, enemies or obstacles or what have you can call one of these generators in order to generate particles.
+
Particles are small moving images that don't interact with the environment.
+
For example, rocks flying around, clouds of gas floating by, or even the trail left behind by thrown Pikmin and leaders.
+
Use the particle editor to help you create particle generators.
+
The particle editor, accessible from the main menu, can help you create particle generators, as well as see how they emit particles and what they look like in real time.
+
Generators can emit any number of particles, and do so only once or over any amount of time.
+
For instance, if an enemy wants to spit a puff of gas, it should generate 10 or so clouds of gas, all at once, and that's the end of it. But if an obstacle wants to continuously spit out thick smoke, it would generate gray clouds over time; it would likely spit a bunch of them every half second or so, to keep a constant stream.
+
Particles have a limited timespan, and can move, change size, and change color.
+
The generator sets the lifetime of each particle it creates, as well as their initial position, movement methods, and how their sizes and colors change over the course of their life.
+
Each generator only emits one type of particle, but their starting properties can be randomized.
+
Some of the generator's properties make it so that every single particle that is generated can have its position, size, and more get deviated randomly. For instance, when a particle of size 32 is created, if the size deviation property is set to 3, that means the particle can spawn with 3 fewer pixels in size, 3 more pixels in size, or anything in between. 0 for any deviation property means no deviation, naturally.
+
+ +

Using the particle editor

+ +
+
The left-hand side is your canvas.
+
It shows you what the particles look like as they're being emitted.
+
The right-hand side contains panels for you to edit properties with.
+
This is how you change the particle generator's properties. As you change them, the emissions in the canvas update in real time, though particles that have already been created may keep some of their old properties.
+
The bottom-left is the status bar. It can help if you get stuck.
+
The status bar will let you know what just happened, whether an operation finished successfully, or something went wrong. If you're confused, check it out.
+
In the canvas, use the left mouse button to move the generator.
+
If you want to know what the particles look like as the generator moves, just drag your mouse around the canvas. This can be helpful to preview how the particles would look on a moving object in-game, for instance.
+
Use the right mouse button to pan, and the mouse wheel to zoom.
+
In the canvas, the right mouse button and mouse wheel control your view. Clicking and double-clicking the mouse wheel will also reset your zoom level and camera position, respectively.
+
The topmost part of the control panel controls the particle previews in the canvas.
+
Use these widgets to pause or unpause the particle system, delete all particles (handy if you've accidentally made the particles linger around for ages!), pause or unpause the particle generator (if it's continuous), make it emit some particles (if it's single-emission), or rotate its facing angle.
+
The bottommost part of the control panel has some common content properties.
+
Use this to give the particle generator a proper name, describe it, and more.
+
+ +

Emission properties

+ +
+
Use the mode properties to choose whether the particle generator emits once, or emits continuously.
+
If you choose to make it emit continuously, you can also specify the interval between emissions, as well as a random deviation of the interval.
+
"Number" controls how many particles spawn per emission.
+
Every time the particle emitter emits particles, it'll create these many. This number also has a random deviation.
+
A circle emission shape makes particles appear in a circle or ring area around the center.
+
To make them appear in a ring area, set "Inner distance" to the radius of the inner circle of the ring. Use "Outer distance" to control the outer circle. Particles will only appear inside this ring, making it useful for something like a large foot stomp that raises dust around its edges. If the inner distance is 0, the shape is effectively a circle instead. You can turn the visibility of the emission shape on in the toolbar to get a better understanding.
+
A rectangular emission shape works the same way, but with a rectangle or rectangular ring.
+
The "Inner distance" property controls the inner rectangle of the rectangular ring, and the "Outer distance" the outer rectangle. As before, you can turn on the visibility of the emission shape to better understand what's going on.
+
+ +

Particle appearance

+ +
+
Use these widgets to set the particle's image, or make it a circle.
+
The image you choose must be one of the images in the game's content. If you instead remove the image, the particles will be flat circles.
+
You can rotate the image, and even make it random.
+
If the image you chose doesn't align properly with what you want the particles to do, just rotate it with the "Angle" property. You can also make it so each particle randomly deviates from this rotation.
+
Particles can have a single color, if you want. This also controls their opacity.
+
To make it so the particles only have one color, remove all keyframes in the "Color" property until only one remains. The keyframe's time does not matter. Then, simply set the color to tint the particle image with, and set the opacity in the alpha component.
+
Particles can have their color change over their lifespan.
+
To make the color change, you make use of multiple keyframes. The color it has when it spawns is controlled by the color information on the first keyframe. You can then add a new keyframe for later on, to indicate that by this or that point, you want the color to be this or that. As the particles grows older in-game, its color will smoothly transition from one keyframe to the next. You can add as many keyframes as you want. A time of 0 means the moment it's born, a time of 0.5 means halfway through its lifespan, a time of 1 is when it gets deleted, etc.
+
Normal blending works like normal, but additive blending makes particles lighter than what's behind them.
+
When a normal particle is drawn on-screen, it just shows its image on top of whatever is behind it, be it other particles or just the game world. But when a particle with additive blending is drawn, it adds its color to the color behind. Meaning the brighter the color behind, the brighter the overall picture will get in that zone. Use this to make it so areas that have a large concentration of additive particles glow bright.
+
Just like the color, a particle can have a fixed size, or have it change over time.
+
This logic works with keyframes, in the exact same way as colors. You can have different keyframes for sizing than those for color.
+
Each particle's individual size can also be deviated randomly.
+
To do this, edit the "Size deviation" property. This will be applied to its size throughout all its life, meaning that if a given particle has a size deviation of +10, and the generator dictates particles start at a size of 50 and grow to a size of 100, this particular particle will start at a size of 60 and grow to size 110.
+
-

As stated before, each generator can only generate one type of particle. The base block inside a generator's data in the data file specifies what the particle looks like. Its size, speed, etc. can all be deviated randomly when it spawns, and that is all controlled by the generator.

- -

The base particle has the following properties:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bitmapImage to use, on the game_data/<pack>/graphics folders. If empty, the particle won't use an image, but will use a colored, solid circle instead.Internal name
durationHow long the particle lives for, in seconds. The closer it is to dying, the more transparent it becomes. This means that it fades with time on its own.Number0
frictionWhen a particle moves, it can slow down over time. The friction controls this. This property specifies the ratio of how much is lost by each second. 0 means no speed loss.Number1
gravityIf set to something other than 0, the created particle is pulled downward each time more over time, like the effects of gravity. This isn't "down" in the game world, this is "down" on the screen, meaning the particles are pulled south, really. Negative values can make the particle rise up each time more. Don't use this to simulate actual gravity on particles, since having a subtle pull to a direction looks good, but a harsh pull southwards may make no sense.Number0
size_grow_speedOver time, its size increases by this much. This is measured in pixels per second. 0 means no change, and negative numbers make it shrink.Number0
sizeStarting size, in pixels. The particle must be square, and this specifies its width or height.Number0
speedSpeed at which it moves horizontally and vertically, in pixels per second. Format: speed = <x> <y>. Naturally, 0 means it stays in place.Coordinates0 0
colorStarting color.Color255 255 255
- -

Generator properties

- -

In the data file, you can specify the following properties to make it generate particles.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
emission_intervalA new bunch of particles is emitted every X seconds. If this is 0, particles are only emitted once.Number0
interval_deviationThe time between every every emission is deviated randomly by this amount.Number0
numberNumber of particles to spawn. Every bunch of particles generated contains X of them.Number1
number_deviationThe number of particles to spawn in each bunch is deviated randomly by this amount.Number0
duration_deviationThe total duration of any created particle is deviated randomly by this amount.Number0
friction_deviationThe friction of any created particle is deviated randomly by this amount.Number0
gravity_deviationThe gravity of any created particle is deviated randomly by this amount.Number0
size_deviationThe size of any created particle is deviated randomly by this amount.Number0
pos_deviationThe X and Y coordinates of any created particle is deviated randomly by this amount.Coordinates0 0
- -

On top of this, there are two ways to control the direction a particle goes to when created:

+

Particle behavior

+ +
+
Each particle has a limited lifespan, controlled by the "Duration" property.
+
This time is dictated in seconds. Each individual particle can also have a deviation on this time.
+
"Linear speed" makes a particle constantly want to move in the specified direction.
+
Use this to make the particle move in a specific direction, with a specific speed. With the use of keyframes, which work like the particle color keyframes, you can make the direction and speed change. All particles will be affected equally by this.
+
Linear speed can have random deviation, both on the X and Y components as well as on the angle.
+
Use the "Speed deviation" and "Angle deviation" properties to make each individual particle apply the specified X and Y speed, or angle shift, throughout its entire life.
+
"Outwards speed" makes a particle constantly want to move away from the generator's center.
+
Just like linear speed, this can be 0, constant, or change with keyframes. This can be combined with linear speed, since both forces take effect during the particle's whole life. Using negative values makes the particles want to move to the generator's center instead.
+
"Orbital speed" makes a particle constantly want to rotate around the generator's center.
+
Just like linear speed and outwards speed, this can be 0, constant, or change with keyframes. This is also combined with linear speed and outwards speed. Negative values make the particles rotate in the opposite direction.
+
Friction slows particles down over time.
+
The higher a particle's friction, the more it will slow down with time. This won't make the particle start moving backwards if it's already at 0 speed. This property can also be randomly deviated.
+
+ +

Tips

- -

To use one of the methods, give the corresponding set of properties a non-zero value, and leave the other set of properties as 0, including deviations. (Alternatively, you can just omit the properties entirely.)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
speed_deviationThe X and Y speed of any created particle is deviated randomly by this amount.Coordinates0 0
angleAbsolute angle to launch the particle at.Number0
angle_deviationThe launch angle of any created particle is deviated randomly by this amount.Number0
total_speedParticles launched with a given angle have this much speed.Number0
total_speed_deviationThe launch speed of any created particle is deviated randomly by this amount.Number0
- -

Examples

- -

For smoke emitted by a fire, the friction can be set to 1, so the particles slow down the more they go, and they can be told to grow in size over time, just like real smoke. For movement, they can be told to go up with a speed of something like -50. The X and Y speed can then be deviated by a bit for variety, but the launch angle and launch speed properties must be set to 0.

- -

For a bunch of clouds of gas emitted in a circle, the friction can again be set to 1, and the size growth can also be positive. However, if we deviate their X and Y speed randomly to obtain a spread, the reach will be that of a square (or rectangle), which wouldn't look very natural. Instead, we leave the X/Y speeds at 0, and set the launch angle to something (0 will work), and set the angle deviation to 360 (a full circle). We must also set the launch speed to something positive, so they actually move. The launch speed can be deviated slightly for variety.

diff --git a/source/documents/todo.txt b/source/documents/todo.txt index 5a54de6a5..db0c03700 100644 --- a/source/documents/todo.txt +++ b/source/documents/todo.txt @@ -5,12 +5,14 @@ Current tasks (tasks being worked on, but not yet committed) Loading the area editor on an area that only has the folder, but not data.txt or geometry.txt crashes The performance monitor isn't monitoring load times for particle generators, maybe others too Check Helodity's particle editor - Add a way to move the emitter with the mouse - A pause/play button for emission, and a pause/play button for the passage of time - Adding a keyframe should go between the previous and the next, and use the interpolated value in the middle - Changing the bitmap while particles exist will result in a SIGSEGV - If a particle generator has a rectangular emission shape and inner == outer, particles will only spawn from the top and bottom, not sides - Update the manual's tutorial + x Add common content properties + x Adding a keyframe should go between the previous and the next, and use the interpolated value in the middle + x Add a way to move the emitter with the mouse + x A way to toggle the grid + x A pause/play button for emission, and a pause/play button for the passage of time + x Arc rotation is not being saved? + x If a particle generator has a rectangular emission shape and inner == outer, particles will only spawn from the top and bottom, not sides + x Update the manual's tutorial Check for memory leaks @@ -24,6 +26,7 @@ Next tasks (roughly sorted most important first) "dismissing when leaving water keeps the water rings" -- Helodity --- 0.26 --- + Optimize the Fiery Blowhog's spritesheet, now that it uses particles Add a "Play" or "Make" label in the corresponding main menu sub-menus All editors should have a way to delete content, not just the area editor Test -S-'s get_onion_info branch @@ -217,9 +220,11 @@ Next tasks (roughly sorted most important first) bud_icon.png and flower_icon.png should be white_bud_icon.png and white_flower_icon.png, to fit with the pink and purple buds and flowers Missions should have a property where the maker can specify their best score (and date) Add a screenshot, or better yet, a gif to the readme - Update screenshots in the manual that still show off the Play area + Manual updates + Update screenshots that still show off the Play area + Have a general editor tutorial, explaining the canvas, status bar, etc., and link to it in the corresponding editor tutorial pages + Have an infobox with quick info about content, like its folder Add the options and stats menu to the pause menu - Remove the word "editor" in the buttons in the main menu. Like "Areas" instead of "Area editor" Cook up a better logo? Format the whole code with Astyle, once #563 is fixed This setup seemed to work ok, or at least as a start: diff --git a/source/source/game_states/animation_editor/drawing.cpp b/source/source/game_states/animation_editor/drawing.cpp index 06284ab75..93eb58b2a 100644 --- a/source/source/game_states/animation_editor/drawing.cpp +++ b/source/source/game_states/animation_editor/drawing.cpp @@ -5,7 +5,7 @@ * Pikmin is copyright (c) Nintendo. * * === FILE DESCRIPTION === - * Animation editor drawing functions. + * Animation editor drawing logic. */ #include diff --git a/source/source/game_states/animation_editor/editor.h b/source/source/game_states/animation_editor/editor.h index 1f20e44bf..45e43d51f 100644 --- a/source/source/game_states/animation_editor/editor.h +++ b/source/source/game_states/animation_editor/editor.h @@ -237,6 +237,9 @@ class animation_editor : public editor { //Whether the dialog needs updating. bool must_update = true; + //Whether we need to focus on the text input widget. + bool needs_text_focus = true; + } new_dialog; diff --git a/source/source/game_states/animation_editor/gui.cpp b/source/source/game_states/animation_editor/gui.cpp index 4a98c140e..f20ef86cf 100644 --- a/source/source/game_states/animation_editor/gui.cpp +++ b/source/source/game_states/animation_editor/gui.cpp @@ -383,7 +383,7 @@ void animation_editor::process_gui_menu_bar() { if(ImGui::BeginMenu("Editor")) { //Load file item. - if(ImGui::MenuItem("Load file...", "Ctrl+L")) { + if(ImGui::MenuItem("Load or create...", "Ctrl+L")) { load_widget_pos = get_last_widget_pos(); load_cmd(1.0f); } @@ -553,9 +553,7 @@ void animation_editor::process_gui_new_dialog() { if(new_dialog.type == 0) { //Internal name input. - if(!ImGui::IsAnyItemActive()) { - ImGui::SetKeyboardFocusHere(); - } + ImGui::FocusOnInputText(new_dialog.needs_text_focus); new_dialog.must_update |= ImGui::InputText("Internal name", &new_dialog.internal_name); set_tooltip( @@ -1580,10 +1578,25 @@ void animation_editor::process_gui_panel_info() { //Panel title text. panel_title("INFO"); + //Name input. + if(ImGui::InputText("Name", &db.name)) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Name of this animation. Optional." + ); + + //Description input. + if(ImGui::InputText("Description", &db.description)) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Description of this animation. Optional." + ); + //Version input. - string version = db.version; - if(ImGui::InputText("Version", &version)) { - db.version = version; + if(ImGui::InputText("Version", &db.version)) { + changes_mgr.mark_as_changed(); } set_tooltip( "Version of the file, preferably in the \"X.Y.Z\" format. " @@ -1591,9 +1604,8 @@ void animation_editor::process_gui_panel_info() { ); //Maker input. - string maker = db.maker; - if(ImGui::InputText("Maker", &maker)) { - db.maker = maker; + if(ImGui::InputText("Maker", &db.maker)) { + changes_mgr.mark_as_changed(); } set_tooltip( "Name (or nickname) of who made this file. " @@ -1601,9 +1613,8 @@ void animation_editor::process_gui_panel_info() { ); //Maker notes input. - string maker_notes = db.maker_notes; - if(ImGui::InputText("Maker notes", &maker_notes)) { - db.maker_notes = maker_notes; + if(ImGui::InputText("Maker notes", &db.maker_notes)) { + changes_mgr.mark_as_changed(); } set_tooltip( "Extra notes or comments about the file for other makers to see. " @@ -1611,9 +1622,8 @@ void animation_editor::process_gui_panel_info() { ); //Notes input. - string notes = db.notes; - if(ImGui::InputText("Notes", ¬es)) { - db.notes = notes; + if(ImGui::InputText("Notes", &db.notes)) { + changes_mgr.mark_as_changed(); } set_tooltip( "Extra notes or comments of any kind. " diff --git a/source/source/game_states/area_editor/drawing.cpp b/source/source/game_states/area_editor/drawing.cpp index 33fa44963..b7e65ec97 100644 --- a/source/source/game_states/area_editor/drawing.cpp +++ b/source/source/game_states/area_editor/drawing.cpp @@ -5,7 +5,7 @@ * Pikmin is copyright (c) Nintendo. * * === FILE DESCRIPTION === - * Area editor drawing function. + * Area editor drawing logic. */ #include diff --git a/source/source/game_states/area_editor/editor.h b/source/source/game_states/area_editor/editor.h index 855370c7f..0aa36c861 100644 --- a/source/source/game_states/area_editor/editor.h +++ b/source/source/game_states/area_editor/editor.h @@ -859,12 +859,14 @@ class area_editor : public editor { string problem; //Path to the new area. - string area_path; //Whether the dialog needs updating. bool must_update = true; + //Whether we need to focus on the text input widget. + bool needs_text_focus = true; + } new_dialog; diff --git a/source/source/game_states/area_editor/gui.cpp b/source/source/game_states/area_editor/gui.cpp index 71a410976..07a673cd7 100644 --- a/source/source/game_states/area_editor/gui.cpp +++ b/source/source/game_states/area_editor/gui.cpp @@ -322,9 +322,7 @@ void area_editor::process_gui_new_dialog() { //Internal name input. ImGui::Spacer(); - if(!ImGui::IsAnyItemActive()) { - ImGui::SetKeyboardFocusHere(); - } + ImGui::FocusOnInputText(new_dialog.needs_text_focus); new_dialog.must_update |= ImGui::InputText("Internal name", &new_dialog.internal_name); set_tooltip( @@ -411,7 +409,7 @@ void area_editor::process_gui_menu_bar() { if(ImGui::BeginMenu("Editor")) { //Load or create area item. - if(ImGui::MenuItem("Load or create area...", "Ctrl+L")) { + if(ImGui::MenuItem("Load or create...", "Ctrl+L")) { load_widget_pos = get_last_widget_pos(); load_cmd(1.0f); } diff --git a/source/source/game_states/editor.cpp b/source/source/game_states/editor.cpp index bf3f012fe..94b76d412 100644 --- a/source/source/game_states/editor.cpp +++ b/source/source/game_states/editor.cpp @@ -777,14 +777,13 @@ bool editor::input_popup( const char* label, const char* prompt, string* text ) { bool ret = false; + needs_input_popup_text_focus = true; if(ImGui::BeginPopup(label)) { if(escape_was_pressed) { ImGui::CloseCurrentPopup(); } ImGui::Text("%s", prompt); - if(!ImGui::IsAnyItemActive()) { - ImGui::SetKeyboardFocusHere(); - } + ImGui::FocusOnInputText(needs_input_popup_text_focus); if( ImGui::InputText( "##inputPopupText", text, @@ -1171,9 +1170,14 @@ bool editor::keyframe_organizer( ImVec2(EDITOR::ICON_BMP_SIZE / 2.0f, EDITOR::ICON_BMP_SIZE / 2.0f) ) ) { - float t = interpolator.get_keyframe(sel_keyframe_idx).first; - inter_t v = interpolator.get_keyframe(sel_keyframe_idx).second; - interpolator.add(t, v); + float prev_t = interpolator.get_keyframe(sel_keyframe_idx).first; + float next_t = + sel_keyframe_idx == interpolator.keyframe_count() - 1 ? + 1.0f : + interpolator.get_keyframe(sel_keyframe_idx + 1).first; + float new_t = (prev_t + next_t) / 2.0f; + + interpolator.add(new_t, interpolator.get(new_t)); sel_keyframe_idx++; set_status( "Added keyframe #" + i2s(sel_keyframe_idx + 1) + "." @@ -1181,8 +1185,8 @@ bool editor::keyframe_organizer( result = true; } set_tooltip( - "Add a new keyframe after the curret one, by copying " - "data from the current one." + "Add a new keyframe after the currently selected one.\n" + "It will go between the current one and the one after." ); if(interpolator.keyframe_count() > 1) { @@ -1207,7 +1211,7 @@ bool editor::keyframe_organizer( result = true; } set_tooltip( - "Delete the current keyframe." + "Delete the currently selected keyframe." ); } @@ -1610,6 +1614,7 @@ void editor::open_message_dialog( * @brief Opens a dialog where the user can create a new pack. */ void editor::open_new_pack_dialog() { + needs_new_pack_text_focus = true; open_dialog( "Create a new pack", std::bind(&editor::process_gui_new_pack_dialog, this) @@ -2231,9 +2236,7 @@ void editor::process_gui_new_pack_dialog() { static string maker; //Internal name input. - if(!ImGui::IsAnyItemActive()) { - ImGui::SetKeyboardFocusHere(); - } + ImGui::FocusOnInputText(needs_new_pack_text_focus); ImGui::InputText("Internal name", &internal_name); set_tooltip( "Internal name of the new pack.\n" @@ -3329,10 +3332,7 @@ void editor::picker_info::process() { "Search filter or new item name" : "Search filter"; - if(!ImGui::IsAnyItemActive() && needs_filter_box_focus) { - ImGui::SetKeyboardFocusHere(); - needs_filter_box_focus = false; - } + ImGui::FocusOnInputText(needs_filter_box_focus); if( ImGui::InputTextWithHint( "##filter", filter_widget_hint.c_str(), &filter, diff --git a/source/source/game_states/editor.h b/source/source/game_states/editor.h index 2163cc352..4c971330b 100644 --- a/source/source/game_states/editor.h +++ b/source/source/game_states/editor.h @@ -638,6 +638,12 @@ class editor : public game_state { //Starting coordinates of a raw mouse drag. point mouse_drag_start; + //Do we need to focus on the input popup's text widget? + bool needs_input_popup_text_focus = true; + + //Do we need to focus on the new pack's name text widget? + bool needs_new_pack_text_focus = true; + //Time left in the operation error red flash effect. timer op_error_flash_timer = timer(EDITOR::OP_ERROR_FLASH_DURATION); diff --git a/source/source/game_states/gui_editor/drawing.cpp b/source/source/game_states/gui_editor/drawing.cpp index 656a2393d..13104e9b8 100644 --- a/source/source/game_states/gui_editor/drawing.cpp +++ b/source/source/game_states/gui_editor/drawing.cpp @@ -5,7 +5,7 @@ * Pikmin is copyright (c) Nintendo. * * === FILE DESCRIPTION === - * GUI editor drawing function. + * GUI editor drawing logic. */ #include "editor.h" diff --git a/source/source/game_states/gui_editor/gui.cpp b/source/source/game_states/gui_editor/gui.cpp index b84da7da5..d53c1d9b0 100644 --- a/source/source/game_states/gui_editor/gui.cpp +++ b/source/source/game_states/gui_editor/gui.cpp @@ -248,7 +248,7 @@ void gui_editor::process_gui_menu_bar() { if(ImGui::BeginMenu("Editor")) { //Load file item. - if(ImGui::MenuItem("Load file...", "Ctrl+L")) { + if(ImGui::MenuItem("Load or create...", "Ctrl+L")) { load_widget_pos = get_last_widget_pos(); load_cmd(1.0f); } diff --git a/source/source/game_states/particle_editor/drawing.cpp b/source/source/game_states/particle_editor/drawing.cpp index 1b878b07b..c7400655d 100644 --- a/source/source/game_states/particle_editor/drawing.cpp +++ b/source/source/game_states/particle_editor/drawing.cpp @@ -21,21 +21,10 @@ * @brief Handles the drawing part of the main loop of the particle editor. */ void particle_editor::do_drawing() { - //Render what is needed for the (Dear ImGui) GUI. - //This will also render the canvas in due time. - ImGui::Render(); + //The canvas drawing is handled by Dear ImGui elsewhere. - //Actually draw the GUI + canvas on-screen. al_clear_to_color(COLOR_BLACK); - ImGui_ImplAllegro5_RenderDrawData(ImGui::GetDrawData()); - draw_op_error_cursor(); - - //And the fade manager atop it all. - game.fade_mgr.draw(); - - //Finally, swap buffers. - al_flip_display(); } @@ -95,79 +84,115 @@ void particle_editor::draw_canvas() { al_use_transform(&game.world_to_screen_transform); - //Grid. - draw_grid( - game.options.particle_editor_grid_interval, - al_map_rgba(64, 64, 64, 84), - al_map_rgba(64, 64, 64, 40) - ); + //Particles. + vector components; + components.reserve(part_mgr.get_count()); + part_mgr.fill_component_list(components, game.cam.box[0], game.cam.box[1]); - //Center grid line. - point cam_top_left_corner(0, 0); - point cam_bottom_right_corner(canvas_br.x, canvas_br.y); - al_transform_coordinates( - &game.screen_to_world_transform, - &cam_top_left_corner.x, &cam_top_left_corner.y - ); - al_transform_coordinates( - &game.screen_to_world_transform, - &cam_bottom_right_corner.x, &cam_bottom_right_corner.y - ); + for(size_t c = 0; c < components.size(); ++c) { + components[c].idx = c; + } - al_draw_line( - 0, cam_top_left_corner.y, 0, cam_bottom_right_corner.y, - al_map_rgb(240, 240, 240), 1.0f / game.cam.zoom - ); - al_draw_line( - cam_top_left_corner.x, 0, cam_bottom_right_corner.x, 0, - al_map_rgb(240, 240, 240), 1.0f / game.cam.zoom + sort( + components.begin(), components.end(), + [](const world_component & c1, const world_component & c2) -> bool { + if(c1.z == c2.z) { + return c1.idx < c2.idx; + } + return c1.z < c2.z; + } ); + for(size_t c = 0; c < components.size(); ++c) { + world_component* c_ptr = &components[c]; + if(c_ptr->particle_ptr) { + c_ptr->particle_ptr->draw(); + } + } - if(emission_offset_visible) { - switch (loaded_gen.emission.shape) { - case(PARTICLE_EMISSION_SHAPE_CIRCLE): + //Grid. + if(grid_visible) { + point cam_top_left_corner(0, 0); + point cam_bottom_right_corner(canvas_br.x, canvas_br.y); + al_transform_coordinates( + &game.screen_to_world_transform, + &cam_top_left_corner.x, &cam_top_left_corner.y + ); + al_transform_coordinates( + &game.screen_to_world_transform, + &cam_bottom_right_corner.x, &cam_bottom_right_corner.y + ); + al_draw_line( + 0, cam_top_left_corner.y, 0, cam_bottom_right_corner.y, + al_map_rgb(240, 240, 240), 1.0f / game.cam.zoom + ); + al_draw_line( + cam_top_left_corner.x, 0, cam_bottom_right_corner.x, 0, + al_map_rgb(240, 240, 240), 1.0f / game.cam.zoom + ); + } + + //Emission shapes. + if(emission_shape_visible) { + switch (loaded_gen.emission.shape) { + case PARTICLE_EMISSION_SHAPE_CIRCLE: { + if(loaded_gen.emission.circle_arc == TAU) { al_draw_circle( - 0, 0, loaded_gen.emission.circle_outer_dist, + generator_pos_offset.x, generator_pos_offset.y, + loaded_gen.emission.circle_outer_dist, al_map_rgb(100, 240, 100), 3.0f / game.cam.zoom ); al_draw_circle( - 0, 0, loaded_gen.emission.circle_inner_dist, + generator_pos_offset.x, generator_pos_offset.y, + loaded_gen.emission.circle_inner_dist, al_map_rgb(240, 100, 100), 3.0f / game.cam.zoom ); } else { al_draw_arc( - 0, 0, loaded_gen.emission.circle_outer_dist, - -loaded_gen.emission.circle_arc / 2 + loaded_gen.emission.circle_arc_rot, loaded_gen.emission.circle_arc, + generator_pos_offset.x, generator_pos_offset.y, + loaded_gen.emission.circle_outer_dist, + -loaded_gen.emission.circle_arc / 2.0f + + loaded_gen.emission.circle_arc_rot + + generator_angle_offset, + loaded_gen.emission.circle_arc, al_map_rgb(100, 240, 100), 3.0f / game.cam.zoom ); al_draw_arc( - 0, 0, loaded_gen.emission.circle_inner_dist, - -loaded_gen.emission.circle_arc / 2 + loaded_gen.emission.circle_arc_rot, loaded_gen.emission.circle_arc, + generator_pos_offset.x, generator_pos_offset.y, + loaded_gen.emission.circle_inner_dist, + -loaded_gen.emission.circle_arc / 2.0f + + loaded_gen.emission.circle_arc_rot + + generator_angle_offset, + loaded_gen.emission.circle_arc, al_map_rgb(240, 100, 100), 3.0f / game.cam.zoom ); - } break; - case(PARTICLE_EMISSION_SHAPE_RECTANGLE): - al_draw_rectangle( - -loaded_gen.emission.rect_outer_dist.x, -loaded_gen.emission.rect_outer_dist.y, - loaded_gen.emission.rect_outer_dist.x, loaded_gen.emission.rect_outer_dist.y, + + } case PARTICLE_EMISSION_SHAPE_RECTANGLE: { + + draw_rotated_rectangle( + generator_pos_offset, + loaded_gen.emission.rect_outer_dist * 2.0f, + generator_angle_offset, al_map_rgb(100, 240, 100), 3.0f / game.cam.zoom ); - al_draw_rectangle( - -loaded_gen.emission.rect_inner_dist.x, -loaded_gen.emission.rect_inner_dist.y, - loaded_gen.emission.rect_inner_dist.x, loaded_gen.emission.rect_inner_dist.y, + draw_rotated_rectangle( + generator_pos_offset, + loaded_gen.emission.rect_inner_dist * 2.0f, + generator_angle_offset, al_map_rgb(240, 100, 100), 3.0f / game.cam.zoom ); break; + + } } } //Leader silhouette. if(leader_silhouette_visible) { - float x_offset = 32; + float x_offset = 32.0f; draw_bitmap( game.sys_assets.bmp_leader_silhouette_top, point(x_offset, 0), @@ -176,31 +201,6 @@ void particle_editor::draw_canvas() { ); } - //Particles. - vector components; - components.reserve(part_mgr.get_count()); - part_mgr.fill_component_list(components, game.cam.box[0], game.cam.box[1]); - - for(size_t c = 0; c < components.size(); ++c) { - components[c].idx = c; - } - - sort( - components.begin(), components.end(), - [](const world_component & c1, const world_component & c2) -> bool { - if(c1.z == c2.z) { - return c1.idx < c2.idx; - } - return c1.z < c2.z; - } - ); - for(size_t c = 0; c < components.size(); ++c) { - world_component* c_ptr = &components[c]; - if(c_ptr->particle_ptr) { - c_ptr->particle_ptr->draw(); - } - } - //Finish up. al_reset_clipping_rectangle(); al_use_transform(&game.identity_transform); diff --git a/source/source/game_states/particle_editor/editor.cpp b/source/source/game_states/particle_editor/editor.cpp index 19926579d..4886887ad 100644 --- a/source/source/game_states/particle_editor/editor.cpp +++ b/source/source/game_states/particle_editor/editor.cpp @@ -59,16 +59,24 @@ particle_editor::particle_editor() : register_cmd( &particle_editor::grid_interval_increase_cmd, "grid_interval_increase" ); + register_cmd(&particle_editor::grid_toggle_cmd, "grid_toggle"); register_cmd(&particle_editor::load_cmd, "load"); register_cmd(&particle_editor::quit_cmd, "quit"); - register_cmd(&particle_editor::particle_playback_toggle_cmd, "toggle_playback"); + register_cmd( + &particle_editor::part_mgr_playback_toggle_cmd, "part_mgr_toggle" + ); + register_cmd( + &particle_editor::part_gen_playback_toggle_cmd, "part_gen_toggle" + ); register_cmd( &particle_editor::leader_silhouette_toggle_cmd, "leader_silhouette_toggle" ); register_cmd(&particle_editor::reload_cmd, "reload"); register_cmd(&particle_editor::save_cmd, "save"); - register_cmd(&particle_editor::zoom_and_pos_reset_cmd, "zoom_and_pos_reset"); + register_cmd( + &particle_editor::zoom_and_pos_reset_cmd, "zoom_and_pos_reset" + ); register_cmd(&particle_editor::zoom_in_cmd, "zoom_in"); register_cmd(&particle_editor::zoom_out_cmd, "zoom_out"); @@ -102,33 +110,40 @@ void particle_editor::close_options_dialog() { /** * @brief Creates a new, empty particle generator. * - * @param requested_name Name of the requested generator. + * @param part_gen_path Path to the new particle generator. */ -void particle_editor::create_particle_generator( - const string &requested_name +void particle_editor::create_part_gen( + const string &part_gen_path ) { - particle_generator new_gen = particle_generator(); - - //Set up some default parameters - new_gen.name = replace_all(requested_name, " ", "_"); - new_gen.base_particle.duration = 1; - new_gen.base_particle.set_bitmap(""); - new_gen.base_particle.size = keyframe_interpolator(32); - new_gen.base_particle.color = keyframe_interpolator(map_alpha(255)); - new_gen.base_particle.color.add(1, map_alpha(0)); - - new_gen.emission.interval = 0.5f; - new_gen.emission.number = 1; - new_gen.base_particle.outwards_speed = keyframe_interpolator(32); - - loaded_gen = new_gen; - - save_options(); //Save the history in the options. - save_part_gen(); //Write the file to disk - setup_for_new_part_gen(); + //Setup. + setup_for_new_part_gen_pre(); + changes_mgr.mark_as_non_existent(); + //Create a particle generator with some defaults. + loaded_gen = particle_generator(); + game.content.custom_particle_gen.path_to_manifest( + part_gen_path, &manifest + ); + loaded_gen.manifest = &manifest; + loaded_gen.base_particle.duration = 1.0f; + loaded_gen.base_particle.set_bitmap(""); + loaded_gen.base_particle.size = + keyframe_interpolator(32); + loaded_gen.base_particle.color = + keyframe_interpolator(map_alpha(255)); + loaded_gen.base_particle.color.add(1, map_alpha(0)); + + loaded_gen.emission.interval = 0.5f; + loaded_gen.emission.number = 1; + loaded_gen.base_particle.outwards_speed = + keyframe_interpolator(32); + + //Finish up. + setup_for_new_part_gen_post(); + update_history(manifest, ""); set_status( - "Created particle \"" + requested_name + "\" successfully." + "Created particle generator \"" + manifest.internal_name + + "\" successfully." ); } @@ -141,11 +156,15 @@ void particle_editor::do_logic() { process_gui(); - if(generator_running) { - loaded_gen.tick(game.delta_t, part_mgr); - //If the particles are meant to be a burst, turn them off. - if(loaded_gen.emission.interval == 0) { - generator_running = false; + if(mgr_running) { + if(gen_running) { + loaded_gen.follow_pos_offset = + rotate_point(generator_pos_offset, -generator_angle_offset); + loaded_gen.tick(game.delta_t, part_mgr); + //If the particles are meant to emit once, turn them off. + if(loaded_gen.emission.interval == 0) { + gen_running = false; + } } part_mgr.tick_all(game.delta_t); } @@ -234,8 +253,6 @@ void particle_editor::load() { ); //Misc. setup. - must_recenter_cam = true; - game.audio.set_current_song(PARTICLE_EDITOR::SONG_NAME, false); part_mgr = particle_manager(game.options.max_particles); @@ -272,7 +289,7 @@ void particle_editor::load_part_gen_file( const string &path, const bool should_update_history ) { //Setup. - setup_for_new_part_gen(); + setup_for_new_part_gen_pre(); changes_mgr.mark_as_non_existent(); //Load. @@ -294,6 +311,7 @@ void particle_editor::load_part_gen_file( loaded_gen.load_from_data_node(&file, CONTENT_LOAD_LEVEL_FULL); //Finish up. + setup_for_new_part_gen_post(); changes_mgr.reset(); if(should_update_history) { @@ -420,6 +438,20 @@ void particle_editor::grid_interval_increase_cmd(float input_value) { } +/** + * @brief Code to run for the grid toggle command. + * + * @param input_value Value of the player input for the command. + */ +void particle_editor::grid_toggle_cmd(float input_value) { + if(input_value < 0.5f) return; + + grid_visible = !grid_visible; + string state_str = (grid_visible ? "Enabled" : "Disabled"); + set_status(state_str + " grid visibility."); +} + + /** * @brief Code to run for the load command. * @@ -485,13 +517,26 @@ void particle_editor::save_cmd(float input_value) { /** * @brief Sets up the editor for a new particle generator, - * be it from an existing file or from scratch. + * be it from an existing file or from scratch, after the actual creation/load + * takes place. + */ +void particle_editor::setup_for_new_part_gen_post() { + loaded_gen.follow_angle = &generator_angle_offset; +} + + +/** + * @brief Sets up the editor for a new particle generator, + * be it from an existing file or from scratch, before the actual creation/load + * takes place. */ -void particle_editor::setup_for_new_part_gen() { +void particle_editor::setup_for_new_part_gen_pre() { part_mgr.clear(); changes_mgr.reset(); - generator_running = true; + mgr_running = true; + gen_running = true; + generator_angle_offset = 0.0f; selected_color_keyframe = 0; selected_size_keyframe = 0; selected_linear_speed_keyframe = 0; @@ -511,7 +556,11 @@ void particle_editor::setup_for_new_part_gen() { void particle_editor::zoom_and_pos_reset_cmd(float input_value) { if(input_value < 0.5f) return; - reset_cam(false); + if(game.cam.target_zoom == 1.0f) { + game.cam.target_pos = point(); + } else { + game.cam.target_zoom = 1.0f; + } } @@ -577,41 +626,60 @@ void particle_editor::clear_particles_cmd(float input_value) { /** - * @brief Code to run for the emission offset toggle command. + * @brief Code to run for the emission shape toggle command. * * @param input_value Value of the player input for the command. */ -void particle_editor::emission_outline_toggle_cmd(float input_value) { +void particle_editor::emission_shape_toggle_cmd(float input_value) { if(input_value < 0.5f) return; - emission_offset_visible = !emission_offset_visible; - string state_str = (emission_offset_visible ? "Enabled" : "Disabled"); - set_status(state_str + " emission offset visibility."); + emission_shape_visible = !emission_shape_visible; + string state_str = (emission_shape_visible ? "Enabled" : "Disabled"); + set_status(state_str + " emission shape visibility."); } /** - * @brief Code to run for the particle playback toggle command. + * @brief Code to run for the particle generator playback toggle command. * * @param input_value Value of the player input for the command. */ -void particle_editor::particle_playback_toggle_cmd(float input_value) { +void particle_editor::part_gen_playback_toggle_cmd(float input_value) { if(input_value < 0.5f) return; - generator_running = !generator_running; - string state_str = (generator_running ? "Enabled" : "Disabled"); - set_status(state_str + " particle playback."); + gen_running = !gen_running; + string state_str = (gen_running ? "Enabled" : "Disabled"); + set_status(state_str + " particle generator playback."); } /** - * @brief Resets the camera. + * @brief Code to run for the particle manager playback toggle command. * - * @param instantaneous Whether the camera moves to its spot instantaneously - * or not. + * @param input_value Value of the player input for the command. + */ +void particle_editor::part_mgr_playback_toggle_cmd(float input_value) { + if(input_value < 0.5f) return; + + mgr_running = !mgr_running; + string state_str = (mgr_running ? "Enabled" : "Disabled"); + set_status(state_str + " particle system playback."); +} + + +/** + * @brief Resets the camera's X and Y coordinates. + */ +void particle_editor::reset_cam_xy() { + game.cam.target_pos = point(); +} + + +/** + * @brief Resets the camera's zoom. */ -void particle_editor::reset_cam(const bool instantaneous) { - center_camera(point(-300.0f, -300.0f), point(300.0f, 300.0f), instantaneous); +void particle_editor::reset_cam_zoom() { + zoom_with_cursor(1.0f); } diff --git a/source/source/game_states/particle_editor/editor.h b/source/source/game_states/particle_editor/editor.h index 54d98cfb0..87b604ad2 100644 --- a/source/source/game_states/particle_editor/editor.h +++ b/source/source/game_states/particle_editor/editor.h @@ -62,23 +62,32 @@ class particle_editor : public editor { //Whether to use a background texture, if any. ALLEGRO_BITMAP* bg = nullptr; + //Is the grid visible? + bool grid_visible = true; + //Picker info for the picker in the "load" dialog. picker_info load_dialog_picker; //Position of the load widget. point load_widget_pos; - //Small hack -- does the camera need recentering in process_gui()? - bool must_recenter_cam = false; + //Is the particle manager currently generating? + bool mgr_running = false; //Is the particle generator currently generating? - bool generator_running = false; + bool gen_running = false; + + //Offset the generator's angle in the editor by this much. + float generator_angle_offset = 0.0f; + + //Offset the generator's position in the editor by this much. + point generator_pos_offset; //Is the leader silhouette visible? bool leader_silhouette_visible = false; - //Is the emission offset visible? - bool emission_offset_visible = false; + //Is the emission shape visible? + bool emission_shape_visible = false; //Selected color keyframe. size_t selected_color_keyframe = 0; @@ -104,11 +113,34 @@ class particle_editor : public editor { //Whether to use a background texture. bool use_bg = false; + struct { + + //Selected pack. + string pack; + + //Internal name of the new particle generator. + string internal_name = "my_particle_generator"; + + //Problem found, if any. + string problem; + + //Path to the new generator. + string part_gen_path; + + //Whether the dialog needs updating. + bool must_update = true; + + //Whether we need to focus on the text input widget. + bool needs_text_focus = true; + + } new_dialog; + + //--- Function declarations --- void close_load_dialog(); void close_options_dialog(); - void create_particle_generator(const string &name); + void create_part_gen(const string &part_gen_path); string get_file_tooltip(const string &path) const; void load_part_gen_file( const string &path, const bool should_update_history @@ -121,13 +153,15 @@ class particle_editor : public editor { void* info, bool is_new ); void reload_part_gens(); - void setup_for_new_part_gen(); + void setup_for_new_part_gen_post(); + void setup_for_new_part_gen_pre(); bool save_part_gen(); static void draw_canvas_imgui_callback( const ImDrawList* parent_list, const ImDrawCmd* cmd ); void grid_interval_decrease_cmd(float input_value); void grid_interval_increase_cmd(float input_value); + void grid_toggle_cmd(float input_value); void load_cmd(float input_value); void quit_cmd(float input_value); void reload_cmd(float input_value); @@ -136,13 +170,15 @@ class particle_editor : public editor { void zoom_in_cmd(float input_value); void zoom_out_cmd(float input_value); void clear_particles_cmd(float input_value); - void emission_outline_toggle_cmd(float input_value); + void emission_shape_toggle_cmd(float input_value); void leader_silhouette_toggle_cmd(float input_value); - void particle_playback_toggle_cmd(float input_value); + void part_gen_playback_toggle_cmd(float input_value); + void part_mgr_playback_toggle_cmd(float input_value); void process_gui(); void process_gui_control_panel(); void process_gui_load_dialog(); void process_gui_menu_bar(); + void process_gui_new_dialog(); void process_gui_options_dialog(); void process_gui_panel_generator(); void process_gui_status_bar(); @@ -161,6 +197,7 @@ class particle_editor : public editor { void handle_rmb_down(const ALLEGRO_EVENT &ev) override; void handle_rmb_drag(const ALLEGRO_EVENT &ev) override; void pan_cam(const ALLEGRO_EVENT &ev); - void reset_cam(const bool instantaneous); + void reset_cam_xy(); + void reset_cam_zoom(); }; diff --git a/source/source/game_states/particle_editor/event_handling.cpp b/source/source/game_states/particle_editor/event_handling.cpp index 8473fbbe0..362b70c10 100644 --- a/source/source/game_states/particle_editor/event_handling.cpp +++ b/source/source/game_states/particle_editor/event_handling.cpp @@ -50,7 +50,7 @@ void particle_editor::handle_key_char_canvas(const ALLEGRO_EVENT &ev) { grid_interval_increase_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_0)) { - reset_cam(false); + zoom_and_pos_reset_cmd(1.0f); } } @@ -62,7 +62,10 @@ void particle_editor::handle_key_char_canvas(const ALLEGRO_EVENT &ev) { * @param ev Event to handle. */ void particle_editor::handle_key_down_anywhere(const ALLEGRO_EVENT &ev) { - if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_L, true)) { + if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_G, true)) { + grid_toggle_cmd(1.0f); + + } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_L, true)) { load_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_Q, true)) { @@ -72,7 +75,10 @@ void particle_editor::handle_key_down_anywhere(const ALLEGRO_EVENT &ev) { save_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_SPACE)) { - particle_playback_toggle_cmd(1.0f); + part_mgr_playback_toggle_cmd(1.0f); + + } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_SPACE, false, true)) { + part_gen_playback_toggle_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_D)) { clear_particles_cmd(1.0f); @@ -81,7 +87,7 @@ void particle_editor::handle_key_down_anywhere(const ALLEGRO_EVENT &ev) { leader_silhouette_toggle_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_R, true)) { - emission_outline_toggle_cmd(1.0f); + emission_shape_toggle_cmd(1.0f); } else if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_ESCAPE)) { escape_was_pressed = true; @@ -100,9 +106,7 @@ void particle_editor::handle_key_down_anywhere(const ALLEGRO_EVENT &ev) { * @param ev Event to handle. */ void particle_editor::handle_key_down_canvas(const ALLEGRO_EVENT &ev) { - if(key_check(ev.keyboard.keycode, ALLEGRO_KEY_HOME)) { - reset_cam(false); - } + } @@ -124,7 +128,7 @@ void particle_editor::handle_lmb_double_click(const ALLEGRO_EVENT &ev) { * @param ev Event to handle. */ void particle_editor::handle_lmb_down(const ALLEGRO_EVENT &ev) { - + generator_pos_offset = game.mouse_cursor.w_pos; } @@ -134,7 +138,7 @@ void particle_editor::handle_lmb_down(const ALLEGRO_EVENT &ev) { * @param ev Event to handle. */ void particle_editor::handle_lmb_drag(const ALLEGRO_EVENT &ev) { - + generator_pos_offset = game.mouse_cursor.w_pos; } @@ -144,7 +148,7 @@ void particle_editor::handle_lmb_drag(const ALLEGRO_EVENT &ev) { * @param ev Event to handle. */ void particle_editor::handle_lmb_up(const ALLEGRO_EVENT &ev) { - + generator_pos_offset = point(); } @@ -156,7 +160,7 @@ void particle_editor::handle_lmb_up(const ALLEGRO_EVENT &ev) { */ void particle_editor::handle_mmb_down(const ALLEGRO_EVENT &ev) { if(!game.options.editor_mmb_pan) { - reset_cam(false); + zoom_and_pos_reset_cmd(1.0f); } } @@ -202,7 +206,7 @@ void particle_editor::handle_mouse_wheel(const ALLEGRO_EVENT &ev) { */ void particle_editor::handle_rmb_down(const ALLEGRO_EVENT &ev) { if(game.options.editor_mmb_pan) { - reset_cam(false); + zoom_and_pos_reset_cmd(1.0f); } } diff --git a/source/source/game_states/particle_editor/gui.cpp b/source/source/game_states/particle_editor/gui.cpp index 349611a26..75c2f2573 100644 --- a/source/source/game_states/particle_editor/gui.cpp +++ b/source/source/game_states/particle_editor/gui.cpp @@ -12,6 +12,7 @@ #include "../../functions.h" #include "../../game.h" +#include "../../libs/imgui/imgui_stdlib.h" #include "../../utils/allegro_utils.h" #include "../../utils/imgui_utils.h" #include "../../utils/string_utils.h" @@ -64,7 +65,20 @@ void particle_editor::open_load_dialog() { * @brief Opens the "new" dialog. */ void particle_editor::open_new_dialog() { - //TODO + new_dialog.must_update = true; + open_dialog( + "Create a new particle generator", + std::bind(&particle_editor::process_gui_new_dialog, this) + ); + dialogs.back()->custom_size = point(400, 0); + dialogs.back()->close_callback = [this] () { + new_dialog.pack.clear(); + new_dialog.internal_name = "my_particle_generator"; + new_dialog.problem.clear(); + new_dialog.part_gen_path.clear(); + new_dialog.must_update = true; + }; + } @@ -118,12 +132,6 @@ void particle_editor::process_gui() { canvas_br.y = br.y; ImGui::GetWindowDrawList()->AddCallback(draw_canvas_imgui_callback, nullptr); - //Small hack. Recenter the camera, if necessary. - if(must_recenter_cam) { - reset_cam(true); - must_recenter_cam = false; - } - //Status bar. process_gui_status_bar(); @@ -232,7 +240,7 @@ void particle_editor::process_gui_menu_bar() { if(ImGui::BeginMenu("Editor")) { //Load file item. - if(ImGui::MenuItem("Load file...", "Ctrl+L")) { + if(ImGui::MenuItem("Load or create...", "Ctrl+L")) { load_widget_pos = get_last_widget_pos(); load_cmd(1.0f); } @@ -369,6 +377,81 @@ void particle_editor::process_gui_menu_bar() { } +/** + * @brief Processes the Dear ImGui "new" dialog for this frame. + */ +void particle_editor::process_gui_new_dialog() { + //Pack widgets. + new_dialog.must_update |= + process_gui_new_dialog_pack_widgets(&new_dialog.pack); + + //Internal name input. + ImGui::Spacer(); + ImGui::FocusOnInputText(new_dialog.needs_text_focus); + new_dialog.must_update |= + ImGui::InputText("Internal name", &new_dialog.internal_name); + set_tooltip( + "Internal name of the new particle generator.\n" + "Remember to keep it simple, type in lowercase, and use underscores!" + ); + + //Check if everything's ok. + if(new_dialog.must_update) { + new_dialog.problem.clear(); + if(new_dialog.internal_name.empty()) { + new_dialog.problem = "You have to type an internal name first!"; + } else if(!is_internal_name_good(new_dialog.internal_name)) { + new_dialog.problem = + "The internal name should only have lowercase letters,\n" + "numbers, and underscores!"; + } else { + content_manifest temp_man; + temp_man.pack = new_dialog.pack; + temp_man.internal_name = new_dialog.internal_name; + new_dialog.part_gen_path = + game.content.custom_particle_gen.manifest_to_path(temp_man); + if(file_exists(new_dialog.part_gen_path)) { + new_dialog.problem = + "There is already a particle generator with\n" + "that internal name in that pack!"; + } + } + new_dialog.must_update = false; + } + + //Create button. + ImGui::Spacer(); + ImGui::SetupCentering(200); + if(!new_dialog.problem.empty()) { + ImGui::BeginDisabled(); + } + if(ImGui::Button("Create particle generator", ImVec2(200, 40))) { + auto really_create = [ = ] () { + create_part_gen(new_dialog.part_gen_path); + close_top_dialog(); + close_top_dialog(); //Close the load dialog. + }; + + if( + new_dialog.pack == FOLDER_NAMES::BASE_PACK && + !game.options.engine_developer + ) { + open_base_content_warning_dialog(really_create); + } else { + really_create(); + } + } + if(!new_dialog.problem.empty()) { + ImGui::EndDisabled(); + } + set_tooltip( + new_dialog.problem.empty() ? + "Create the particle generator!" : + new_dialog.problem + ); +} + + /** * @brief Processes the options dialog for this frame. */ @@ -494,23 +577,27 @@ void particle_editor::process_gui_options_dialog() { * @brief Processes the particle generator panel for this frame. */ void particle_editor::process_gui_panel_generator() { + //Particle system text. + ImGui::Text("Particle system:"); + //Particle count text. + ImGui::Indent(); ImGui::Text( - "Particle count: %lu / %lu", + "Particles: %lu / %lu", part_mgr.get_count(), game.options.max_particles ); - //Play/pause button. + //Play/pause particle system button. if( ImGui::ImageButton( - "playButton", - generator_running ? + "playSystemButton", + mgr_running ? editor_icons[EDITOR_ICON_STOP] : editor_icons[EDITOR_ICON_PLAY], ImVec2(EDITOR::ICON_BMP_SIZE, EDITOR::ICON_BMP_SIZE) ) ) { - particle_playback_toggle_cmd(1.0f); + part_mgr_playback_toggle_cmd(1.0f); } set_tooltip( "Play or pause the particle system.", @@ -532,6 +619,41 @@ void particle_editor::process_gui_panel_generator() { set_tooltip( "Delete all existing particles." ); + ImGui::Unindent(); + + //Particle generator text. + ImGui::Text("Generator:"); + + //Play/pause particle generator button. + ImGui::Indent(); + if( + ImGui::ImageButton( + "playGeneratorButton", + gen_running ? + editor_icons[EDITOR_ICON_STOP] : + editor_icons[EDITOR_ICON_PLAY], + ImVec2(EDITOR::ICON_BMP_SIZE, EDITOR::ICON_BMP_SIZE) + ) + ) { + part_gen_playback_toggle_cmd(1.0f); + } + set_tooltip( + loaded_gen.emission.interval == 0.0f ? + "Emit particles now." : + "Play or pause the particle generator's emission.", + "Shift + Spacebar" + ); + + //Particle generator angle value. + ImGui::SameLine(); + ImGui::SetNextItemWidth(85); + ImGui::SliderAngle("Angle", &generator_angle_offset, 0.0f); + set_tooltip( + "Rotate the generator's facing angle in the editor by this much.\n" + "You can move the generator by just dragging the mouse in the canvas.", + "", WIDGET_EXPLANATION_DRAG + ); + ImGui::Unindent(); //Emission node. ImGui::Spacer(); @@ -868,6 +990,9 @@ void particle_editor::process_gui_panel_generator() { ImVec2(EDITOR::ICON_BMP_SIZE, EDITOR::ICON_BMP_SIZE) ) ) { + //We can't have living particles with destroyed bitmaps, + //so clear them all. + part_mgr.clear(); loaded_gen.base_particle.set_bitmap(""); changes_mgr.mark_as_changed(); } @@ -883,6 +1008,9 @@ void particle_editor::process_gui_panel_generator() { ) { open_bitmap_dialog( [this] (const string &bmp) { + //We can't have living particles with destroyed bitmaps, + //so clear them all. + part_mgr.clear(); loaded_gen.base_particle.set_bitmap(bmp); changes_mgr.mark_as_changed(); set_status("Picked an image successfully."); @@ -1162,7 +1290,8 @@ void particle_editor::process_gui_panel_generator() { saveable_tree_node("generatorBehavior", "Outwards speed"); set_tooltip( "Control the speed at which a particle moves out from\n" - "the center here." + "the center here. Use negative values to make them move\n" + "towards the center instead." ); if(open_outwards_speed_node) { @@ -1255,6 +1384,82 @@ void particle_editor::process_gui_panel_generator() { ImGui::TreePop(); } + + //Info node. + ImGui::Spacer(); + bool open_info_node = + saveable_tree_node("generator", "Info"); + set_tooltip( + "Optional information about the particle generator." + ); + if(open_info_node) { + + //Name input. + if( + ImGui::InputText("Name", &loaded_gen.name) + ) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Name of this particle generator. Optional." + ); + + //Description input. + if( + ImGui::InputText("Description", &loaded_gen.description) + ) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Description of this particle generator. Optional." + ); + + //Version input. + if( + ImGui::InputText("Version", &loaded_gen.version) + ) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Version of the file, preferably in the \"X.Y.Z\" format. " + "Optional." + ); + + //Maker input. + if( + ImGui::InputText("Maker", &loaded_gen.maker) + ) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Name (or nickname) of who made this file. " + "Optional." + ); + + //Maker notes input. + if( + ImGui::InputText("Maker notes", &loaded_gen.maker_notes) + ) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Extra notes or comments about the file for other makers to see. " + "Optional." + ); + + //Notes input. + if(ImGui::InputText("Notes", &loaded_gen.notes)) { + changes_mgr.mark_as_changed(); + } + set_tooltip( + "Extra notes or comments of any kind. " + "Optional." + ); + + ImGui::TreePop(); + + } + } @@ -1339,7 +1544,24 @@ void particle_editor::process_gui_toolbar() { "Ctrl + S" ); + //Toggle grid button. ImGui::SameLine(0, 16); + if( + ImGui::ImageButton( + "gridButton", + editor_icons[EDITOR_ICON_GRID], + ImVec2(EDITOR::ICON_BMP_SIZE, EDITOR::ICON_BMP_SIZE) + ) + ) { + grid_toggle_cmd(1.0f); + } + set_tooltip( + "Toggle visibility of the grid.", + "Ctrl + G" + ); + + //Leader silhouette button. + ImGui::SameLine(); if( ImGui::ImageButton( "silhouetteButton", @@ -1354,18 +1576,19 @@ void particle_editor::process_gui_toolbar() { "Ctrl + P" ); + //Emission shape button. ImGui::SameLine(); if( ImGui::ImageButton( - "particleOffsetButton", + "emissionShapeButton", editor_icons[EDITOR_ICON_MOB_RADIUS], ImVec2(EDITOR::ICON_BMP_SIZE, EDITOR::ICON_BMP_SIZE) ) ) { - emission_outline_toggle_cmd(1.0f); + emission_shape_toggle_cmd(1.0f); } set_tooltip( - "Toggle visibility of the emission deviation.", + "Toggle visibility of the emission shape.", "Ctrl + R" ); } diff --git a/source/source/particle.cpp b/source/source/particle.cpp index b6d68d9fa..50f443ae1 100644 --- a/source/source/particle.cpp +++ b/source/source/particle.cpp @@ -51,34 +51,40 @@ particle::particle( * @brief Draws this particle onto the world. */ void particle::draw() { - ALLEGRO_COLOR target_color = color.get((duration - time) / duration); - float target_size = size.get((duration - time) / duration); + float t = 1.0f - time / duration; + ALLEGRO_COLOR final_color = color.get(t); + float final_size = size.get(t); + bool used_custom_blend = false; int old_op, old_source, old_dest; - al_get_blender(&old_op, &old_source, &old_dest); switch(blend_type) { - case PARTICLE_BLEND_TYPE_ADDITIVE: + case PARTICLE_BLEND_TYPE_ADDITIVE: { + al_get_blender(&old_op, &old_source, &old_dest); al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_ONE); + used_custom_blend = true; break; - default: + } default: { break; } + } if(bitmap) { draw_bitmap( - bitmap, pos, point(target_size, -1), - bmp_angle, target_color + bitmap, pos, point(final_size, -1), + bmp_angle, final_color ); } else { al_draw_filled_circle( pos.x, pos.y, - target_size * 0.5, - target_color + final_size * 0.5, + final_color ); } - al_set_blender(old_op, old_source, old_dest); + if(used_custom_blend) { + al_set_blender(old_op, old_source, old_dest); + } } @@ -96,19 +102,22 @@ void particle::tick(const float delta_t) { return; } - point total_velocity = linear_speed.get(1 - (time / duration)); + float t = 1.0f - time / duration; + point total_velocity = linear_speed.get(t); float outwards_angle = get_angle(pos - origin); if(pos == origin) { outwards_angle = randomf(-180, 180); } - total_velocity += angle_to_coordinates(outwards_angle, outwards_speed.get(1 - (time / duration))); - - //Add 90 degrees to make the angle tangential - total_velocity += angle_to_coordinates(outwards_angle + (TAU / 4), orbital_speed.get(1 - (time / duration))); - - //Accumulate and apply friction + total_velocity += + angle_to_coordinates(outwards_angle, outwards_speed.get(t)); + + //Add 90 degrees to make the angle tangential. + total_velocity += + angle_to_coordinates(outwards_angle + (TAU / 4), orbital_speed.get(t)); + + //Accumulate and apply friction. total_velocity -= total_friction_applied; point new_friction = total_velocity * (delta_t* friction); total_friction_applied += new_friction; @@ -142,7 +151,10 @@ void particle::set_bitmap( } if(new_file_name != file || !bitmap) { - bitmap = game.content.bitmaps.list.get(new_file_name, node, node != nullptr); + bitmap = + game.content.bitmaps.list.get( + new_file_name, node, node != nullptr + ); } file = new_file_name; @@ -198,10 +210,13 @@ void particle_generator::emit(particle_manager &manager) { std::max( 0, (int) emission.number + - randomi((int) (0 - emission.number_deviation), (int)emission.number_deviation) + randomi( + (int) (0 - emission.number_deviation), + (int) emission.number_deviation + ) ); - for(size_t p = 0; p < final_nr; ++p) { + for(size_t p = 0; p < final_nr; p++) { particle new_p = base_particle; new_p.duration = @@ -234,28 +249,36 @@ void particle_generator::emit(particle_manager &manager) { } float angle_to_use = - randomf(-linear_speed_angle_deviation, linear_speed_angle_deviation); + randomf( + -linear_speed_angle_deviation, + linear_speed_angle_deviation + ); if(follow_angle) angle_to_use += (*follow_angle); - float v_dev_x = randomf(-linear_speed_deviation.x, linear_speed_deviation.x); - float v_dev_y = randomf(-linear_speed_deviation.y, linear_speed_deviation.y); + float v_dev_x = + randomf(-linear_speed_deviation.x, linear_speed_deviation.x); + float v_dev_y = + randomf(-linear_speed_deviation.y, linear_speed_deviation.y); for(size_t s = 0; s < new_p.linear_speed.keyframe_count(); s++) { auto kf = new_p.linear_speed.get_keyframe(s); point base = kf.second; point result = point(base.x + v_dev_x, base.y + v_dev_y); - result = rotate_point( - result, angle_to_use - ); + result = + rotate_point( + result, angle_to_use + ); new_p.linear_speed.set_keyframe_value(s, result); } - float out_dev = randomf(-outwards_speed_deviation, outwards_speed_deviation); + float out_dev = + randomf(-outwards_speed_deviation, outwards_speed_deviation); for(size_t s = 0; s < new_p.outwards_speed.keyframe_count(); s++) { auto kf = new_p.outwards_speed.get_keyframe(s); new_p.outwards_speed.set_keyframe_value(s, kf.second + out_dev); } - float orb_dev = randomf(-orbital_speed_deviation, orbital_speed_deviation); + float orb_dev = + randomf(-orbital_speed_deviation, orbital_speed_deviation); for(size_t s = 0; s < new_p.orbital_speed.keyframe_count(); s++) { auto kf = new_p.orbital_speed.get_keyframe(s); new_p.orbital_speed.set_keyframe_value(s, kf.second + orb_dev); @@ -300,21 +323,22 @@ void particle_generator::load_from_data_node( ers.set("shape", shape_int); switch (shape_int) { - case PARTICLE_EMISSION_SHAPE_CIRCLE: + case PARTICLE_EMISSION_SHAPE_CIRCLE: { ers.set("max_radius", emission.circle_outer_dist); ers.set("min_radius", emission.circle_inner_dist); ers.set("arc", emission.circle_arc); + ers.set("arc_rotation", emission.circle_arc_rot); break; - case PARTICLE_EMISSION_SHAPE_RECTANGLE: + } case PARTICLE_EMISSION_SHAPE_RECTANGLE: { ers.set("max_offset", emission.rect_outer_dist); ers.set("min_offset", emission.rect_inner_dist); break; } + } - emission.shape = (PARTICLE_EMISSION_SHAPE)shape_int; + emission.shape = (PARTICLE_EMISSION_SHAPE) shape_int; data_node* bitmap_node = nullptr; - size_t blend_int = 0; prs.set("bitmap", base_particle.file, &bitmap_node); @@ -323,14 +347,24 @@ void particle_generator::load_from_data_node( prs.set("friction", base_particle.friction); prs.set("blend_type", blend_int); - base_particle.blend_type = (PARTICLE_BLEND_TYPE)blend_int; + base_particle.blend_type = (PARTICLE_BLEND_TYPE) blend_int; base_particle.bmp_angle = deg_to_rad(base_particle.bmp_angle); - base_particle.color.load_from_data_node(base_particle_node->get_child_by_name("color")); - base_particle.size.load_from_data_node(base_particle_node->get_child_by_name("size")); - base_particle.linear_speed.load_from_data_node(base_particle_node->get_child_by_name("linear_speed")); - base_particle.outwards_speed.load_from_data_node(base_particle_node->get_child_by_name("outwards_speed")); - base_particle.orbital_speed.load_from_data_node(base_particle_node->get_child_by_name("orbital_speed")); + base_particle.color.load_from_data_node( + base_particle_node->get_child_by_name("color") + ); + base_particle.size.load_from_data_node( + base_particle_node->get_child_by_name("size") + ); + base_particle.linear_speed.load_from_data_node( + base_particle_node->get_child_by_name("linear_speed") + ); + base_particle.outwards_speed.load_from_data_node( + base_particle_node->get_child_by_name("outwards_speed") + ); + base_particle.orbital_speed.load_from_data_node( + base_particle_node->get_child_by_name("orbital_speed") + ); if(bitmap_node) { if(level >= CONTENT_LOAD_LEVEL_FULL) { @@ -376,39 +410,75 @@ void particle_generator::save_to_data_node( data_node* emission_particle_node = new data_node("emission", ""); node->add(emission_particle_node); - emission_particle_node->add(new data_node("number", i2s(emission.number))); - emission_particle_node->add(new data_node("number_deviation", i2s(emission.number_deviation))); - emission_particle_node->add(new data_node("interval", f2s(emission.interval))); - emission_particle_node->add(new data_node("interval_deviation", f2s(emission.interval_deviation))); - emission_particle_node->add(new data_node("shape", i2s(emission.shape))); + emission_particle_node->add( + new data_node("number", i2s(emission.number)) + ); + emission_particle_node->add( + new data_node("number_deviation", i2s(emission.number_deviation)) + ); + emission_particle_node->add( + new data_node("interval", f2s(emission.interval)) + ); + emission_particle_node->add( + new data_node("interval_deviation", f2s(emission.interval_deviation)) + ); + emission_particle_node->add( + new data_node("shape", i2s(emission.shape)) + ); switch (emission.shape) { - case PARTICLE_EMISSION_SHAPE_CIRCLE: - emission_particle_node->add(new data_node("max_radius", f2s(emission.circle_outer_dist))); - emission_particle_node->add(new data_node("min_radius", f2s(emission.circle_inner_dist))); - emission_particle_node->add(new data_node("arc", f2s(emission.circle_arc))); + case PARTICLE_EMISSION_SHAPE_CIRCLE: { + emission_particle_node->add( + new data_node("max_radius", f2s(emission.circle_outer_dist)) + ); + emission_particle_node->add( + new data_node("min_radius", f2s(emission.circle_inner_dist)) + ); + emission_particle_node->add( + new data_node("arc", f2s(emission.circle_arc)) + ); + emission_particle_node->add( + new data_node("arc_rotation", f2s(emission.circle_arc_rot)) + ); break; - case PARTICLE_EMISSION_SHAPE_RECTANGLE: - emission_particle_node->add(new data_node("max_offset", p2s(emission.rect_outer_dist))); - emission_particle_node->add(new data_node("min_offset", p2s(emission.rect_inner_dist))); + } case PARTICLE_EMISSION_SHAPE_RECTANGLE: { + emission_particle_node->add( + new data_node("max_offset", p2s(emission.rect_outer_dist)) + ); + emission_particle_node->add( + new data_node("min_offset", p2s(emission.rect_inner_dist)) + ); break; } + } data_node* base_particle_node = new data_node("base", ""); node->add(base_particle_node); - base_particle_node->add(new data_node("bitmap", base_particle.file)); - base_particle_node->add(new data_node("rotation", f2s(rad_to_deg(base_particle.bmp_angle)))); - base_particle_node->add(new data_node("duration", f2s(base_particle.duration))); - base_particle_node->add(new data_node("friction", f2s(base_particle.friction))); - base_particle_node->add(new data_node("blend_type", i2s(base_particle.blend_type))); + base_particle_node->add( + new data_node("bitmap", base_particle.file) + ); + base_particle_node->add( + new data_node("rotation", f2s(rad_to_deg(base_particle.bmp_angle))) + ); + base_particle_node->add( + new data_node("duration", f2s(base_particle.duration)) + ); + base_particle_node->add( + new data_node("friction", f2s(base_particle.friction)) + ); + base_particle_node->add( + new data_node("blend_type", i2s(base_particle.blend_type)) + ); data_node* color_node = new data_node("color", ""); base_particle_node->add(color_node); for(size_t c = 0; c < base_particle.color.keyframe_count(); c++) { auto keyframe = base_particle.color.get_keyframe(c); - color_node->add(new data_node(f2s(keyframe.first), c2s(keyframe.second))); + color_node->add( + new data_node(f2s(keyframe.first), c2s(keyframe.second)) + ); } data_node* size_node = new data_node("size", ""); @@ -416,7 +486,9 @@ void particle_generator::save_to_data_node( for(size_t c = 0; c < base_particle.size.keyframe_count(); c++) { auto keyframe = base_particle.size.get_keyframe(c); - size_node->add(new data_node(f2s(keyframe.first), f2s(keyframe.second))); + size_node->add( + new data_node(f2s(keyframe.first), f2s(keyframe.second)) + ); } data_node* lin_speed_node = new data_node("linear_speed", ""); @@ -424,7 +496,9 @@ void particle_generator::save_to_data_node( for(size_t c = 0; c < base_particle.linear_speed.keyframe_count(); c++) { auto keyframe = base_particle.linear_speed.get_keyframe(c); - lin_speed_node->add(new data_node(f2s(keyframe.first), p2s(keyframe.second))); + lin_speed_node->add( + new data_node(f2s(keyframe.first), p2s(keyframe.second)) + ); } data_node* out_speed_node = new data_node("outwards_speed", ""); @@ -432,7 +506,9 @@ void particle_generator::save_to_data_node( for(size_t c = 0; c < base_particle.outwards_speed.keyframe_count(); c++) { auto keyframe = base_particle.outwards_speed.get_keyframe(c); - out_speed_node->add(new data_node(f2s(keyframe.first), f2s(keyframe.second))); + out_speed_node->add( + new data_node(f2s(keyframe.first), f2s(keyframe.second)) + ); } data_node* orb_speed_node = new data_node("orbital_speed", ""); @@ -440,17 +516,35 @@ void particle_generator::save_to_data_node( for(size_t c = 0; c < base_particle.orbital_speed.keyframe_count(); c++) { auto keyframe = base_particle.orbital_speed.get_keyframe(c); - orb_speed_node->add(new data_node(f2s(keyframe.first), f2s(keyframe.second))); + orb_speed_node->add( + new data_node(f2s(keyframe.first), f2s(keyframe.second)) + ); } - node->add(new data_node("rotation_deviation", f2s(rad_to_deg(bmp_angle_deviation)))); - node->add(new data_node("duration_deviation", f2s(duration_deviation))); - node->add(new data_node("friction_deviation", f2s(friction_deviation))); - node->add(new data_node("size_deviation", f2s(size_deviation))); - node->add(new data_node("orbital_speed_deviation", f2s(orbital_speed_deviation))); - node->add(new data_node("outwards_speed_deviation", f2s(outwards_speed_deviation))); - node->add(new data_node("angle_deviation", f2s(rad_to_deg(linear_speed_angle_deviation)))); - node->add(new data_node("linear_speed_deviation", p2s(linear_speed_deviation))); + node->add( + new data_node("rotation_deviation", f2s(rad_to_deg(bmp_angle_deviation))) + ); + node->add( + new data_node("duration_deviation", f2s(duration_deviation)) + ); + node->add( + new data_node("friction_deviation", f2s(friction_deviation)) + ); + node->add( + new data_node("size_deviation", f2s(size_deviation)) + ); + node->add( + new data_node("orbital_speed_deviation", f2s(orbital_speed_deviation)) + ); + node->add( + new data_node("outwards_speed_deviation", f2s(outwards_speed_deviation)) + ); + node->add( + new data_node("angle_deviation", f2s(rad_to_deg(linear_speed_angle_deviation))) + ); + node->add( + new data_node("linear_speed_deviation", p2s(linear_speed_deviation)) + ); } @@ -490,7 +584,9 @@ void particle_generator::tick(float delta_t, particle_manager &manager) { } else { emission_timer = randomf( - std::max(0.0f, emission.interval - emission.interval_deviation), + std::max( + 0.0f, emission.interval - emission.interval_deviation + ), emission.interval + emission.interval_deviation ); } @@ -624,7 +720,8 @@ void particle_manager::fill_component_list( for(size_t c = 0; c < count; c++) { particle* p_ptr = &particles[c]; - float p_size = p_ptr->size.get((p_ptr->duration - p_ptr->time) / p_ptr->duration); + float p_size = + p_ptr->size.get((p_ptr->duration - p_ptr->time) / p_ptr->duration); if( cam_tl != cam_br && !rectangles_intersect( @@ -720,36 +817,33 @@ particle_emission_struct::particle_emission_struct( } +/** + * @brief Returns a randomly-picked offset for a new particle. + * + * @return The offset. + */ point particle_emission_struct::get_emission_offset() { switch (shape) { case PARTICLE_EMISSION_SHAPE_CIRCLE: { - //Created using - //https://stackoverflow.com/questions/30564015/how-to-generate-random-points-in-a-circular-distribution - float r = circle_inner_dist + (circle_outer_dist - circle_inner_dist) * sqrt(randomf(0, 1)); - + return + get_random_point_in_ring( + circle_inner_dist, circle_outer_dist, + circle_arc, circle_arc_rot + ); + break; - float theta = randomf( - -circle_arc / 2 + circle_arc_rot, - circle_arc / 2 + circle_arc_rot - ); - - return point(r * cos(theta), r * sin(theta)); - } - case PARTICLE_EMISSION_SHAPE_RECTANGLE: { - //Not perfectly uniform, but it works. - int xSign = (randomi(0, 1) * 2) - 1; - int ySign = (randomi(0, 1) * 2) - 1; + } case PARTICLE_EMISSION_SHAPE_RECTANGLE: { + return + get_random_point_in_rectangular_ring( + rect_inner_dist, rect_outer_dist + ); + break; - float x = randomf(0, rect_outer_dist.x); - float y = x < rect_inner_dist.x ? randomf(rect_inner_dist.y, rect_outer_dist.y) : randomf(0, rect_outer_dist.y); + } default: { + return point(); + break; - return point( - x * xSign, - y * ySign - ); } - default: - return point(0, 0); } } diff --git a/source/source/particle.h b/source/source/particle.h index cfa17f95e..bed7b6013 100644 --- a/source/source/particle.h +++ b/source/source/particle.h @@ -300,7 +300,7 @@ struct particle_generator : public content { //Follow the given mob's coordinates. mob* follow_mob = nullptr; - //Offset the follow mob coordinates by this. + //Offset the follow mob coordinates by this, relative to the mob angle. point follow_pos_offset; //Offset the follow mob Z by this. diff --git a/source/source/utils/geometry_utils.cpp b/source/source/utils/geometry_utils.cpp index da66306e2..fc2c9f952 100644 --- a/source/source/utils/geometry_utils.cpp +++ b/source/source/utils/geometry_utils.cpp @@ -1057,6 +1057,107 @@ float get_point_sign(const point &p, const point &lp1, const point &lp2) { } +/** + * @brief Returns a random point inside of a rectangular ring, with uniform + * distribution. + * + * @param inner_dist Width and height of the inner rectangle of the ring. + * @param outer_dist Width and height of the outer rectangle of the ring. + * @return The point. + */ +point get_random_point_in_rectangular_ring( + const point &inner_dist, const point &outer_dist +) { + float ring_thickness[2] { + outer_dist.x - inner_dist.x, + outer_dist.y - inner_dist.y + }; + + //The idea is to split the ring into four rectangles, organized in a + //pinwheel pattern. + //In this pattern, the north and south rectangles have the exact same area, + //and the same is true for the west and east ones. We can simplify the + //process with this in mind. + point rect_sizes[2] = { + point( + ring_thickness[0], + outer_dist.y * 2.0f - ring_thickness[1] + ), + point( + outer_dist.x * 2.0f - ring_thickness[0], + ring_thickness[1] + ) + }; + float rect_areas[2] = { + rect_sizes[0].x* rect_sizes[0].y, + rect_sizes[1].x* rect_sizes[1].y + }; + + //Pick one of the four rectangles (or in this case, one of the two axes), + //with weighted probability depending on the area. + size_t chosen_axis; + if(rect_areas[0] == 0.0f && rect_areas[1] == 0.0f) { + chosen_axis = randomi(0, 1); + } else { + chosen_axis = randomw(vector(rect_areas, rect_areas + 2)); + } + + point p_in_rectangle( + randomf(0.0f, rect_sizes[chosen_axis].x), + randomf(0.0f, rect_sizes[chosen_axis].y) + ); + point final_p; + + if(chosen_axis == 0) { + //West or east rectangle. Let's assume the east rectangle. + final_p.x = inner_dist.x + p_in_rectangle.x, + final_p.y = -outer_dist.y + p_in_rectangle.y; + } else { + //North or south rectangle. Let's assume the south rectangle. + final_p.x = -inner_dist.x + p_in_rectangle.x; + final_p.y = inner_dist.y + p_in_rectangle.y; + } + + if(randomi(0, 1) == 0) { + //Return our point. + return final_p; + } else { + //Swap to the rectangle on the opposite side. + return point() - final_p; + } +} + + +/** + * @brief Returns a random point inside of a circular ring, with uniform + * distribution. + * + * @param inner_dist Radius of the inner circle of the ring. + * @param outer_dist Radius of the outer circle of the ring. + * @param arc Arc of the ring, or M_TAU for the whole ring. + * @param arc_rot Rotation of the arc. + * @return The point. + */ +point get_random_point_in_ring( + float inner_dist, float outer_dist, + float arc, float arc_rot +) { + //https://stackoverflow.com/q/30564015 + + float r = + inner_dist + + (outer_dist - inner_dist) * sqrt(randomf(0.0f, 1.0f)); + + float theta = + randomf( + -arc / 2.0f + arc_rot, + arc / 2.0f + arc_rot + ); + + return point(r * cos(theta), r * sin(theta)); +} + + /** * @brief Gets the bounding box coordinates of a rectangle that has undergone * translation, scale, and/or rotation transformations, and places it diff --git a/source/source/utils/geometry_utils.h b/source/source/utils/geometry_utils.h index 171f6454d..c0dc37046 100644 --- a/source/source/utils/geometry_utils.h +++ b/source/source/utils/geometry_utils.h @@ -184,6 +184,13 @@ void get_miter_points( float get_point_sign( const point &p, const point &lp1, const point &lp2 ); +point get_random_point_in_rectangular_ring( + const point &inner_dist, const point &outer_dist +); +point get_random_point_in_ring( + float inner_dist, float outer_dist, + float arc, float arc_rot +); void get_transformed_rectangle_bounding_box( const point ¢er, const point &dimensions, float angle, point* min_coords, point* max_coords diff --git a/source/source/utils/imgui_utils.cpp b/source/source/utils/imgui_utils.cpp index 4397283f4..26853dff4 100644 --- a/source/source/utils/imgui_utils.cpp +++ b/source/source/utils/imgui_utils.cpp @@ -116,13 +116,13 @@ bool ImGui::Combo( label, ¤t_item_idx, item_display_names, popup_max_height_in_items ); - + if(current_item_idx == -1) { current_item->clear(); } else { *current_item = item_internal_values[current_item_idx]; } - + return result; } @@ -181,6 +181,13 @@ bool ImGui::DragTime2( return result; } +void ImGui::FocusOnInputText(bool &condition) { + if(!ImGui::IsAnyItemActive() && condition) { + ImGui::SetKeyboardFocusHere(); + condition = false; + } +} + /** * @brief Helps creating an ImGui ImageButton, followed by a centered Text. diff --git a/source/source/utils/imgui_utils.h b/source/source/utils/imgui_utils.h index 9a325d44a..de99073f5 100644 --- a/source/source/utils/imgui_utils.h +++ b/source/source/utils/imgui_utils.h @@ -44,6 +44,7 @@ bool DragTime2( const string &format1 = "m", const string &format2 = "s", int limit1 = INT_MAX, int limit2 = 59 ); +void FocusOnInputText(bool &condition); bool ImageButtonAndText( const string &id, ALLEGRO_BITMAP* icon, const ImVec2 &icon_size, float button_padding, const string &text diff --git a/source/source/utils/math_utils.cpp b/source/source/utils/math_utils.cpp index 937478272..d49a20d80 100644 --- a/source/source/utils/math_utils.cpp +++ b/source/source/utils/math_utils.cpp @@ -229,6 +229,27 @@ int randomi(int minimum, int maximum) { } +/** + * @brief Performs a weighted random pick, and returns the index of the chosen + * item. + * + * @param weights A vector with the weight of each item. + * @return Index of the chosen item, or 0 on error. + */ +size_t randomw(const vector &weights) { + float weight_sum = 0.0f; + for(size_t i = 0; i < weights.size(); i++) { + weight_sum += weights[i]; + } + float r = randomf(0.0f, weight_sum); + for(size_t i = 0; i < weights.size(); i++) { + if(r < weights[i]) return i; + r -= weights[i]; + } + return 0.0f; +} + + /** * @brief Sums a number to another (even if negative), and then * wraps that number across a limit, applying a modulus operation. diff --git a/source/source/utils/math_utils.h b/source/source/utils/math_utils.h index 7bd36bac1..b712e45ef 100644 --- a/source/source/utils/math_utils.h +++ b/source/source/utils/math_utils.h @@ -17,16 +17,20 @@ #include #include #include +#include + +using std::vector; + constexpr float TAU = (float)M_PI * 2.0f; //Methods for easing numbers. enum EASING_METHOD { - + //No easing. AKA linear interpolation. EASE_METHOD_NONE, - + //Eased as it goes in, then gradually goes out normally. EASE_METHOD_IN, @@ -77,5 +81,6 @@ float interpolate_number( ); float randomf(float min, float max); int randomi(int min, int max); +size_t randomw(const vector &weights); int sum_and_wrap(int nr, int sum, int wrap_limit); float wrap_float(float nr, float minimum, float maximum);