Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - eri0o

#241
Probably best to have it at 3.6.2 branch where it's still in beta and we can test with a bunch of old games and see if it doesn't happen to break anything, and later it may be just a bugfix that isn't backported - I think if the current behavior was an issue in some old game, something would have cropped up from the ScummVm testers.

Quote from: Crimson Wizard on Sun 27/10/2024 18:32:18When Dialog.Start is called within a script, it's actually scheduled to run after script completes, similar to ChangeRoom, and few other things.
So, this "post-script" execution seems a convenient place to do this.
I found a possible trick: while processing this scheduled command, check if we are inside a dialog, and then not start a new dialog loop, but override the script's return value that is going to be passed back into the current running dialog loop. This return value contains a instruction of what to do (stop dialog, switch to another topic, etc).
In a way, Dialog.Start called within a dialog script will work more like a postponed "goto-dialog".

This sounds like it would work great. I think it would give the expected behavior as an adventure game even though it may not be as consistent when think purely as a programmer (that would expect the nested dialogs).
#242
While warning about the nesting somehow is best, I wonder if, since there should be a way to test that it is inside a dialog, if there would be a way that in such case it could be delayed to happen after. At least I don't feel that the nesting is expected - even though it happens. The issue there is it would trigger a dialog end and start event by doing this way - at least I think?
#243
You can add a button to toggle from

Code: ags
System.Window != System.Window;
#244
I haven't looked but that is the default in AGS, each character has its own inventory. In the inventory window there's a property that you just set the character the inventory is supposed to match to - you can just pass the player character too after you change it.

I know because I used this to make my first game in ags, which was a card game and each player had its own hand using each ones inventory.
#245
OK, just switching for software renderer and setting full log and it still crashes

https://gist.github.com/ericoporto/c247bcb751153c43a73cf3fa6990258b

(for anyone reading the log, I think once it crashes the chronological order of the log is reversed...)

Just curious, the character cUomoAppeso is in the room before it becomes the player character? Is there any fun code around the place that it happens? Also does view 26 have something weird vs other views?
#246
Spoiler
OK, let's start with the basics and see if you are talking about something like Logic Grid Puzzles. Example below

QuotePuzzle Description:
Lucy is a sought-after cake maker in the friendly small town of Plumpton. There aren't enough residents in the town to run her business full-time, so she bakes and decorates the cakes in the evenings after her main job. Tomorrow night, Lucy will be busy with three cakes to prepare - all different flavors and occasions!

From the clues given, can you determine which client (identified by first name) ordered which type of cake, what the occasion is, and the price each customer will pay?

Clues
  • Emma ordered carrot cake for an event she is hosting at her home, but it did not cost $37.00.
  • Attendees of the farewell party will each be enjoying a big slice of vanilla cake.
  • The cake Michael ordered cost $2.00 less than the cake that is for a birthday.
  • The chocolate cake is Lucy's specialty, which she currently has discounted to $35.00.





It looks like it depends a lot on the size of the grid you wish to make if it's something like this. Essentially you would need to support three states per "cell" in the grid. I guess it also depends on the resolution of what you are doing - like, if it's anything above 320x200 I would suggest to avoid using dynamic sprites. Can you give at least a drawing of the scale of what you want to make?
[close]

Actually I don't think I understand what is Einstein game. I really wish people used more images here... Googling for Einstein game it appears its something like Simon says. I am a bit confused, there appears there is also a weird board game. I put in spoiler because I skipped the Einstein game on the first read of your question and read the description that happens later.
#247
Hey, I decided to post it to the module topic here, it's still early stages, but just in the hopes someone picks it up and play with it to get some feedback on it.

Also if someone reads the doc and has questions or can't understand something, please ask clarifications. I have some drawings I plan to at some point redo on the computer that I hope can help explaining the system in place a bit.
#248
Particles version 0.1.0

Get Latest Release particles.scm | GitHub Repo | Project with Demo!



Particle Script Module for AGS

Overview

This module is meant to help you manage particles on screen, which can be used for visual effects or even for game related actions.

It has two base concepts, the emitter, which is the source and owner of the effect you want to devise, and the particles, which run a small piece of logic repeatedly and syncs itself to a visual representation as AGS Overlays.

There is an additional concept which this module uses called the particle definition, which is a simple object that has no logic, but it has a series of properties, which represents the initial configuration and instructions a particle will receive, including characteristics it should have, how it should behave (using physics concepts) and how it should look.

An emitter owns a collection of particle definitions (an array), and when it emits each particle it selects a particle definition from its collection at random, and assigns it to a particle, which is then emitted.

Because this collection of definitions can be big, you can use any random process you want (with your own probability distributions) to create this collection. Because the emitter samples this collection at random, the produced particles will have matching distribution (at least visually) to the one used to generate the array of definitions.


Usage

Here is a simple example, to quickstart using this module and give an idea of how it works

Code: ags
// ... in room script
// encapsulate definition in a function to be able to apply randomnes
ParticleDefinition* GetSparkleParticle()
{
  ParticleDefinition* sparkleParticle = new ParticleDefinition;
  sparkleParticle.LifeTotal = 50;
  sparkleParticle.VelX = Random(3000) - 1000;
  sparkleParticle.VelY = Random(3000) - 1000;
  sparkleParticle.TransparencyBegin = 0;
  sparkleParticle.TransparencyEnd = 100;
  sparkleParticle.WidthBegin = 3;
  sparkleParticle.WidthEnd = 8;
  sparkleParticle.HeightBegin = 3;
  sparkleParticle.HeightEnd = 8;
  sparkleParticle.Gravity = 100;
  sparkleParticle.GroundY = 154;
  sparkleParticle.Bounces = true;
  return sparkleParticle;
}

Emitter emt;

void room_AfterFadeIn()
{
  // Create array of particle definitions
  int defs_count = 2048;
  ParticleDefinition *defs[] = new ParticleDefinition[defs_count];
  for(i=0; i<defs_count; i++)
  {
    defs[i] = GetSparkleParticle();
  }
  
  // Emitter at (150, 90) emitting 10 particles, max 256 at a time
  emt.Init(150, 90, defs, defs_count, 10,  256);
}

void on_mouse_click(MouseButton button)
{
  // Emit particles on click
  emt.SetPosition(mouse.x, mouse.y);
  emt.Emit();
}

function repeatedly_execute_always()
{
  emt.Update();
}



Script API
Spoiler

Emitter

This struct is the main way we will manage particles in this module.

Emitter.Init
Code: ags
void Emitter.Init(int x, int y, ParticleDefinition * defs[], int defCount, int emitAmount, int maxParticles);
Initializes the emitter, has to be run once, before invoking any other method from the emitter.

You will pass the following parameters

- a position (x, y) to place the emitter (it can be set later to other using SetPosition method)
- an array of Particle Definitions, along with the size of this array
- the amount of particles that should be emitted when calling Emit()
- the maximum number of particles that should exist at the same time

Emitter.Update
Code: ags
void Emitter.Update();
This method will both run one step in the particle simulation and update their rendering on the screen using overlays.

You normally run this once in repeatedly_execute_always, room_RepExec or some other method you use to run once per frame.

Emitter.Emit
Code: ags
void Emitter.Emit();
Emits particles. The amount of particles emitted is the emitAmount set when you init the emitter.

A random particle definition is selected from the set definitions arrays, and used to initialize each particle emitted individually.

Emitter.SetPosition
Code: ags
void Emitter.SetPosition(int x, int y);
Sets the position of the emitter on screen.

Emitter.SetDefinitions
Code: ags
void Emitter.SetDefinitions(ParticleDefinition * defs[], int defCount);
Sets the definitions hold by the emitter.

Emitter.ParticlesHitPoint
Code: ags
Particle * [] Emitter.ParticlesHitPoint(int x, int y);
Get null terminated array of particles that overlaps with the given point.

Emitter.ParticlesHitRect
Code: ags
Particle * [] Emitter.ParticlesHitRect(int x, int y, int width, int height);
Get null terminated array of particles that overlaps with the given rectangle.


Particle

This struct represents a single particle in the particle system. It is used to simulate the movement, appearance, and behavior of each particle.

It's managed by the emitter and they can be retrieved through specific methods in the emitter (for now only hit tests), and then you can get some information from each directly.

Particle.Life
Code: ags
int attribute Particle.Life;
The remaining life of the particle. It decrements on each update and the particle dies when its life is equal to or below zero.

Particle.IsAlive
Code: ags
bool Particle.IsAlive();
Returns true if the particle is still alive (i.e., its life is greater than zero), and false otherwise.

Particle.HitsPoint
Code: ags
bool Particle.HitsPoint(int x, int y);
Returns true if the particle overlaps the given point (x, y). The particle is assumed to be a rectangle for the purpose of hit detection.

Particle.HitsRect
Code: ags
bool Particle.HitsRect(int x, int y, int width, int height);
Returns true if the particle overlaps the given rectangle (x, y, width, height). The particle is assumed to be a rectangle for the purpose of hit detection.


ParticleDefinition

This struct defines the behavior and visual properties of particles. It is used by the emitter to generate new particles with specific characteristics.

When you set the value of them, it's usually a good idea to create a function to encapsulate this setting, so you can produce many different values with random settings on each definition.

ParticleDefinition.Sprite
Code: ags
int ParticleDefinition.Sprite;

The sprite used for the particle. If SpriteBegin and SpriteEnd are set, this defines the initial frame of the particle animation.

ParticleDefinition.OffsetX
Code: ags
int ParticleDefinition.OffsetX;
The horizontal offset from the emitter's position when the particle is emitted.

ParticleDefinition.OffsetY
Code: ags
int ParticleDefinition.OffsetY;
The vertical offset from the emitter's position when the particle is emitted.

ParticleDefinition.LifeTotal
Code: ags
int ParticleDefinition.LifeTotal;
The total lifetime of the particle, in update loops. This value is used to initialize the particle's life when it is emitted.

ParticleDefinition.VelX
Code: ags
int ParticleDefinition.VelX;
The initial horizontal velocity of the particle, in thousandths of a pixel per update loop. It's in X direction, so positive numbers moves particle to right.

ParticleDefinition.VelY
Code: ags
int ParticleDefinition.VelY;
The initial vertical velocity of the particle, in thousandths of a pixel per update loop. It's in Y direction, so positive numbers moves particle upwards.

ParticleDefinition.Gravity
Code: ags
int ParticleDefinition.Gravity;
The vertical acceleration applied to the particle over time (gravity), in thousandths of a pixel per update loop.

ParticleDefinition.SpriteBegin
Code: ags
int ParticleDefinition.SpriteBegin;
The initial sprite frame of a sequential sprite range.

ParticleDefinition.SpriteEnd
Code: ags
int ParticleDefinition.SpriteEnd;
The final sprite frame of a sequential sprite range.

ParticleDefinition.TransparencyBegin
Code: ags
int ParticleDefinition.TransparencyBegin;
The transparency level when the particle is emitted. A value of 0 is fully opaque, and 100 is fully transparent.

ParticleDefinition.TransparencyEnd
Code: ags
int ParticleDefinition.TransparencyEnd;
The transparency level when the particle reaches the end of its life.

ParticleDefinition.WidthBegin
Code: ags
int ParticleDefinition.WidthBegin;
The width of the particle when it is emitted.

ParticleDefinition.WidthEnd
Code: ags
int ParticleDefinition.WidthEnd;
The width of the particle when it reaches the end of its life.

ParticleDefinition.HeightBegin
Code: ags
int ParticleDefinition.HeightBegin;
The height of the particle when it is emitted.

ParticleDefinition.HeightEnd
Code: ags
int ParticleDefinition.HeightEnd;
The height of the particle when it reaches the end of its life.

ParticleDefinition.Bounces
Code: ags
bool ParticleDefinition.Bounces;
Determines whether the particle should bounce when it hits the ground.

ParticleDefinition.GroundY
Code: ags
int ParticleDefinition.GroundY;
The vertical position that the particle will treat as the ground for bounce detection. If this is not set, the particle will not recognize any ground.

ParticleDefinition.BlendMode
Code: ags
BlendMode ParticleDefinition.BlendMode;
The blend mode to use when rendering the particle.

Compatibility: This is only available in AGS 4.0 and above.

ParticleDefinition.Angle
Code: ags
float ParticleDefinition.Angle;
The initial rotation angle of the particle, in degrees (0 to 360).

Compatibility: This is only available in AGS 4.0 and above.

ParticleDefinition.RotationSpeed
Code: ags
float ParticleDefinition.RotationSpeed;
The speed at which the particle rotates, in degrees per update loop.

Compatibility: This is only available in AGS 4.0 and above.

[close]




License
This module is created by eri0o is provided with MIT License, see LICENSE for more details.

Note

This module is considered in beta stage, and there is probably a lot missing in it, as an example it can't yet produce room overlays (I haven't figured how I should manage them in the pool that is used behind the scenes).

Please play with it and give me feedback and ideas you have. Still, there is a lot that can be done with it already!
#249
Run it here: https://ericoporto.github.io/public_html/ags_362_cm_744406cd444c/


paste the error you get in the console under a code tag and inside a spoiler here in the forums. This build should give more complete error log from the console in chrome dev tools - I mean the specific JS messages, they should mention the specific lines in the ags engine code where it's hitting an issue.

Or perhaps if you have an updated build somewhere, and could share it?

Edit: I tried running the old build and using the wheel to get to the error, but the error is somehow in a part of the wasm where it appears it doesn't map to a source file??? (wth, how?)

And also the callstack is showing only things in Emscripten itself... I have never caused something like this, so I have no idea, it would be interesting to get the game source to see in the script what is being called at such point that could cause this. I wonder which ags features the game is using too - things like lipsync and other too classic adventure stuff I basically never used, so I have no idea if it's a bug in one of those old parts of the code somehow... Anyway, it would be nice to get the code to try to comeup with an easier way to reproduce this, since I would like to instrument the code but these make running anything really really slow and doing so much stuff to reproduce is painful.

error.log
Code: plaintext
ags.html:615 Aborted(RuntimeError: unreachable)
printErr @ ags.html:615
abort @ ags.js:439
runAndAbortIfError @ ags.js:12064
maybeStopUnwind @ ags.js:12163
ret.<computed> @ ags.js:12114
doRewind @ ags.js:12212
(anonymous) @ ags.js:12238
callUserCallback @ ags.js:4919
(anonymous) @ ags.js:7257
setTimeout
safeSetTimeout @ ags.js:7255
(anonymous) @ ags.js:11861
handleSleep @ ags.js:12223
_emscripten_sleep @ ags.js:11861
$Emscripten_GLES_SwapWindow @ SDL_emscriptenopengles.c:93
$byn$fpcast-emu$Emscripten_GLES_SwapWindow @ ags.wasm:0x7cb61f
$SDL_GL_SwapWindowWithResult @ SDL_video.c:4205
$SDL_GL_SwapWindow @ SDL_video.c:4210
$AGS::Engine::OGL::OGLGraphicsDriver::Render(int, int, AGS::Common::GraphicFlip) @ ali3dogl.cpp:1181
$byn$fpcast-emu$AGS::Engine::OGL::OGLGraphicsDriver::Render(int, int, AGS::Common::GraphicFlip) @ ags.wasm:0x6f022e
$AGS::Engine::OGL::OGLGraphicsDriver::Render() @ ali3dogl.cpp:965
$byn$fpcast-emu$AGS::Engine::OGL::OGLGraphicsDriver::Render() @ ags.wasm:0x6f014a
$dynCall_vi @ ags.wasm:0x6e3956
ret.<computed> @ ags.js:12110
invoke_vi @ ags.js:13305
$render_to_screen() @ draw.cpp:1198
$render_graphics(AGS::Engine::IDriverDependantBitmap*, int, int) @ draw.cpp:2958
$UpdateGameOnce(bool, AGS::Engine::IDriverDependantBitmap*, int, int) @ game_run.cpp:1027
$GameTick() @ game_run.cpp:1148
$GameLoopUntilState::Run() @ game_run.cpp:184
$byn$fpcast-emu$GameLoopUntilState::Run() @ ags.wasm:0x736b0c
$dynCall_ii @ ags.wasm:0x6e3866
ret.<computed> @ ags.js:12110
invoke_ii @ ags.js:13239
$GameLoopUntilEvent(int, void const*, int, int) @ game_run.cpp:1165
$GameLoopUntilNoOverlay() @ game_run.cpp:1210
$display_main(int, int, int, char const*, TopBarSettings const*, int, int, int, int, int, bool, bool) @ display.cpp:441
$_displayspeech(char const*, int, int, int, int, int) @ character.cpp:2911
$Sc_Character_Say(void*, RuntimeScriptValue const*, int) @ character.cpp:2956
$byn$fpcast-emu$Sc_Character_Say(void*, RuntimeScriptValue const*, int) @ ags.wasm:0x75d775
$ccInstance::Run(int) @ cc_instance.cpp:1363
$ccInstance::CallScriptFunction(AGS::Common::String const&, int, RuntimeScriptValue const*) @ cc_instance.cpp:463
$byn$fpcast-emu$ccInstance::CallScriptFunction(AGS::Common::String const&, int, RuntimeScriptValue const*) @ ags.wasm:0x78bdd5
$dynCall_iiiii @ ags.wasm:0x6e3e04
ret.<computed> @ ags.js:12110
invoke_iiiii @ ags.js:13327
...
#250
I tried to build the AGS engine for debugging. I didn't attempt to build the exact version you used but instead I decided to build the one in this commit here https://github.com/adventuregamestudio/ags/tree/744406cd444ca935685d235ca8368af71f7d1154 (it was current master when I built).

I actually put a build of it here publicly, along with the corresponding ags source files, so one can run it on the Chrome Dev Tools debugger and follow along in the cpp files. You just attach your game files there and it should start.

Unfortunately, I am having an error much earlier, in Step 6, I started the conversation with the hanging man, but as I understand to do step 7 I need to click to scroll the dialogs to get to the sixth option, but unfortunately clicking on the scroll hangs the game. You can easily reproduce it by running in the url above.

I put a release build in agsjs website, using the same commit, just to test if it's some issue with the debug build, but unfortunately, it reproduces.

Could you give me the code for how you make that scroll in the dialog? I would like to figure the minimal thing necessary to reproduce that bug. After that one can be fixed we can try moving to the other.

Spoiler
Also I see a few messages in the console reggarding the save

Code: ags
22:59:28:194: Unable to read save's description.
agsjs/:1 22:59:28:194: File not found or could not be opened.
agsjs/:1 22:59:28:195: Requested filename: /home/web_user/saved_games/Lightning strike in town festival/agssave.052.
agsjs/:1 22:59:28:198: Unable to read save's description.
agsjs/:1 22:59:28:198: File not found or could not be opened.
agsjs/:1 22:59:28:198: Requested filename: /home/web_user/saved_games/Lightning strike in town festival/agssave.053.
agsjs/:1 22:59:28:203: Unable to read save's screenshot.
agsjs/:1 22:59:28:204: File not found or could not be opened.
agsjs/:1 22:59:28:204: Requested filename: /home/web_user/saved_games/Lightning strike in town festival/agssave.050.
agsjs/:1 22:59:28:204: Unable to read save's screenshot.
agsjs/:1 22:59:28:204: File not found or could not be opened.
agsjs/:1 22:59:28:204: Requested filename: /home/web_user/saved_games/Lightning strike in town festival/agssave.050.
agsjs/:1 22:59:28:205: Unable to read save's screenshot.
agsjs/:1 22:59:28:205: File not found or could not be opened.
agsjs/:1 22:59:28:205: Requested filename: /home/web_user/saved_games/Lightning strike in town festival/agssave.050.

I guess you hardcoded the save files in the gui, the nice approach is to fill an out of screen or invisible list box with the saves and then retrieving them from the listbox.
[close]
#251
  • There is a code formatting option in the forums, it's better to use it to ensure any code doesn't get interpreted as tag and disappears
  • When mentioning an error message, it's a good idea to paste here the text of the error message - usually just hitting Ctrl+C with the message box that pops up will copy the text. Alternatively you can screenshot and use an image host like imgur to link the image directly here (there is also an image tag in the forums!)

Other than this I am not sure how you made your gGui1, what it has and it would be nice to check the functions from it are actually linked to the globalscript by checking the events tab - I know you said you did, but since there's a mysterious error message and it's not commented what you are seeing it's hard to tell what could be wrong. :/

Ah, right, just to check the error isn't file access, one good idea is running one of the templates and checking if you can save and load with them.
#252
Thanks CW, those are very good remarks! Switched the hacked address to instead using a simple int as uID, and now on init of the emitter if it's not set yet, the global uID is incremented and the value is assigned to the emitter.

The emitter is not a managed struct, it's a simple struct, and it's alright to linger a little in the global set, since the global set is cleared on every room transition.

It seems that I finally got the code to be bug free, I will mostly do a few additional tests to ensure I am not doing anything stupid and I think I will publish a beta version of the module sometime this week.

I ended up stealing your object pool design to the management of overlays, and I think at some point I will also do it for the management of particles, since both are intended to be handled as pools - but in the case of the particles I will probably leave this part of refactoring to a bit in the future since it's mostly meant to improve performance and avoid unnecessary loops.
#253
Code: ags
struct Emitter {
  ...
  String address; // <<-- added this!
  ...
};

OK, came up with a really ugly solution that works. I discovered I can get the address of the current struct using String.Format("%p", this) in the Init method, so I do it and note down in the emitter address property. This works even if the struct is not managed!

I put a global Set, and then when I run the Update of an emitter, I check if it exists in this global set by looking if the set contains it's address, if it doesn't I do what I have to do assuming the room has changed and finally add the emitter address to the Set - so this only actually runs once per room.

Finally, whenever there is a room change, I clear the emitter Set entirely. A single additional if in an Update call is ok performance wise, and tracking only emitters is much lighter than tracking all particles. Next time the update will run for that emitter it will know a room change has happened and deal with it.
#254
Just wanted to comment that there is a lot very smartly written in this module, the logic for acquiring and releasing in a deterministic way without for loops is not something I would have been able to think by myself. Loved the logic of tracking both the free and the in use, great stuff.

I have done the idea of a pool in my own stuff before and always did it very inefficiently and didn't notice  until I decided to check this module.
#255
OK, one more iteration of it, I think the header is shaping nicely. If someone can, please take a look and tell me what makes sense or not of it.

header
ParticleAccelerator.ash
Code: ags
// Particle Accelerator module header
// Module to manage particles effects based on Overlays
//
//    +-------------------------+
//    |        Emitter          |
//    |-------------------------|      +--------------------+
//    |  Array of Definitions  |<------| ParticleDefinition |
//    |  Pool of Particles      |      +--------------------+
//    +-------------------------+        (sprite, velocity, size, etc.)
//                |
//                |    Emits based on a random
//                |  selection from definitions
//                v
//    +--------------------+
//    |      Particle      |
//    +--------------------+
// 
// The Emitter is how we will manage particles in this module.
// It's responsible for a few things
// 1. When emitting it randomly selects a ParticleDefinition and Spawns it;
// 2. Updates the particle;
// 3. Renders the particle as overlay until it's life ends.

managed struct ParticleDefinition {
  /// The particle sprite. If SpriteBegin/End are set, it's the initial frame. 
  int Sprite;
  /// Horizontal Offset from the emitter position
  int OffsetX;
  /// Vertical Offset from the emitter position
  int OffsetY;
  /// The initial life of the particle, it's Lifetime in update loops
  int life;
  /// Horizontal mili velocity. It's in thousandths of a pixel per update, in X direction (e.g., 1000 moves 1 pixel to right per update).
  int VelX; 
  /// Vertical mili velocity. It's in thousandths of a pixel per update, in Y direction (e.g., -2000 moves 2 pixel upwards per update).
  int VelY;
  /// Mili Gravity (vertical acceleration).
  int Gravity;
  /// The initial sprite of a sequential Sprite range (see Sprite for initial frame).
  int SpriteBegin;
  /// The final sprite of a sequential Sprite range.
  int SpriteEnd;
  /// Initial transparency, the Transparency when emitted.
  int TransparencyBegin;
  /// Final transparency, the Transparency when Particle life is zero.
  int TransparencyEnd;
  /// Initial width, the Width when emitted.
  int WidthBegin;
  /// Final width, the Width when Particle life is zero.
  int WidthEnd;
  /// Initial height, the Height when emitted.
  int HeightBegin;
  /// Final height, the Height when Particle life is zero.
  int HeightEnd;
  /// If the particle should bounce when it hits ground
  bool Bounces;
  /// The ground level position for the particle (if unset assumes no ground exists)
  int GroundY;
  #ifdef SCRIPT_API_v400
  /// The blend mode the particle should use
  BlendMode BlendMode;
  /// The angle in degrees (0.0 to 360.0) the particle should be
  float Angle;
  /// The speed that will increase the angle in degrees per update loop
  float RotationSpeed;
  #endif
};

managed struct Particle {
  /// The particle life, it decrements on each update. It dies when life is equal or below zero.
  import attribute int Life;
  /// returns true if particle is alive
  import bool IsAlive();
  /// returns true if particle overlaps the given point. Particle is assumed a rectangle.
  import bool HitsPoint(int x, int y);
  /// returns true if particle overlaps the given rectangle. Particles is assumed a rectangle.
  import bool HitsRect(int x, int y, int width, int height);

  // private internals
  protected int X;
  protected int Y;
  protected int MiliX; // mili x (~1000 times x)
  protected int MiliY; // mili y (~1000 times y)
  protected int Sprite;
  protected int InitialLife;
  protected int VelX; // x velocity
  protected int VelY; // y velocity
  protected int Gravity; // this is vertical acceleration downwards
  protected int Transparency;
  protected int Width;
  protected int Height;
  protected int SpriteBegin;
  protected int SpriteCycleOffset;
  protected int SpriteDelta;
  protected int TransparencyBegin;
  protected int TransparencyDelta;
  protected int WidthBegin;
  protected int WidthDelta;
  protected int HeightBegin;
  protected int HeightDelta;
  protected bool Bounces;
  protected int GroundY;
  #ifdef SCRIPT_API_v400
  protected float RotationSpeed;
  protected float Angle;
  #endif
  // not actual public interface and should not be used or relied upon
  int _Life; // $AUTOCOMPLETEIGNORE$
  int _OverlayIdx; // $AUTOCOMPLETEIGNORE$
};

struct Emitter {
  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition * defs[], int defCount, int emitAmount, int maxParticles);
  /// Emit particles set in emitBurst, returns true if succeed emitting all particles
  import bool Emit();
  /// Update all particles
  import void Update();
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition * definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);
  /// Get null terminated array of particles that overlaps with the given point.
  import Particle * [] ParticlesHitPoint(int x, int y);
  /// Get null terminated array of particles that overlaps with the given rectangle.
  import Particle * [] ParticlesHitRect(int x, int y, int width, int height);

  // private internals
  protected int X;
  protected int Y;
  protected int EmitAmount;
  protected int maxParticles;
  protected Particle * particles[]; // Pool of particles
  protected ParticleDefinition * definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  protected int lastEmittedParticle;
  import protected bool _EmitParticleIndex(int i);
  import protected bool _EmitSingleParticle();
};

struct ContinuousEmitter extends Emitter {
  protected int emitInterval;
  protected int _emitCooldown;
  protected bool isEmitting;

  /// Starts the emission of particles at regular intervals. Interval unit is in update loops.
  import void StartEmitting(int emitInterval = 11);
  /// Stops the emission of particles.
  import void StopEmitting();
  import void UpdateContinuous();
};

/// Global Particle Emitter
import ContinuousEmitter GPE;

script
ParticleAccelerator.asc
Spoiler
Code: ags
// Particle Accelerator module script

#define LERP_SCALE 1024
#define INT_FRAC 1024
#define MAX_OVERLAYS 2048
Overlay *overlayPool[MAX_OVERLAYS];
bool overlayUsed[MAX_OVERLAYS];
int lastUsed;

int FreeOverlay (int index)
{
  if (overlayPool[index] != null)
  {
    overlayPool[index].Transparency = 100;
  }
  overlayUsed[index] = false; // Mark the overlay slot as free
  return -1;
}

// Find an available overlay slot in the pool, remember the slot to speed up search
int FindFreeOverlaySlot ()
{
  for (int i = lastUsed + 1; i < MAX_OVERLAYS; i++)
  {
    if (!overlayUsed[i])
    {
      overlayUsed[i] = true;
      lastUsed = i;
      return i;
    }
  }
  for (int i = 0; i < lastUsed; i++)
  {
    if (!overlayUsed[i])
    {
      overlayUsed[i] = true;
      lastUsed = i;
      return i;
    }
  }
  return -1; // No available overlay
}

// ---------------------- Particle methods ----------------------------

// we will use extenders in internal methods
void _SyncOverlay (this Particle*, Overlay* ovr)
{
  ovr.Transparency = this.Transparency;
  ovr.Graphic = this.Sprite;
#ifdef SCRIPT_API_v362
  ovr.SetPosition (this.X, this.Y, this.Width, this.Height);
#else
  ovr.X = this.X;
  ovr.Y = this.Y;
  ovr.Width = this.Width;
  ovr.Height = this.Height;
#endif
#ifdef SCRIPT_API_v400
  ovr.Rotation = this.Angle;
#endif
}

void _Init (this Particle*, ParticleDefinition *def, int x, int y, Overlay* ovr)
{
  this.MiliX = INT_FRAC * (x + def.OffsetX); // Offset based on emitter
  this.MiliY = INT_FRAC * (y + def.OffsetY); // Offset based on emitter
  this.X = this.MiliX / INT_FRAC;
  this.Y = this.MiliY / INT_FRAC;
  this._Life = def.life;
  this.VelX = (def.VelX * INT_FRAC) / 1000;
  this.VelY = (def.VelY * INT_FRAC) / 1000;
  this.Gravity = (def.Gravity * INT_FRAC) / 1000;

  this.Transparency = def.TransparencyBegin;
  this.TransparencyBegin = def.TransparencyBegin;
  this.TransparencyDelta = def.TransparencyEnd - def.TransparencyBegin;

  this.Width = def.WidthBegin;
  this.WidthBegin = def.WidthBegin;
  this.WidthDelta = def.WidthEnd - def.WidthBegin;

  this.Height = def.HeightBegin;
  this.HeightBegin = def.HeightBegin;
  this.HeightDelta = def.HeightEnd - def.HeightBegin;

  this.SpriteCycleOffset = def.Sprite - def.SpriteBegin;
  this.Sprite = def.Sprite;
  this.SpriteBegin = def.SpriteBegin;
  this.SpriteDelta = def.SpriteEnd - def.SpriteBegin;
  
#ifdef SCRIPT_API_v400
  this.RotationSpeed = def.RotationSpeed;
  this.Angle = def.Angle;
#endif

  this.InitialLife = def.life; // Store initial life for transitions
  this.Bounces = def.Bounces;
  this.GroundY = def.GroundY;
  if (this.GroundY <= 0)
  {
    this.GroundY = 16777216; // a big number so it is not reached
  }
  
#ifdef SCRIPT_API_v400
  ovr.BlendMode = def.BlendMode;
#endif
  this._SyncOverlay(ovr);
}

// Update the particle state and sync with overlay
void _Update (this Particle*)
{
  // alive check is done before calling this function
  this.MiliX += this.VelX;
  this.MiliY += this.VelY;
  this.VelY += this.Gravity; // Apply Gravity
  this.X = this.MiliX / INT_FRAC;
  this.Y = this.MiliY / INT_FRAC;

  // Calculate the scaling and Transparency transitions based on life
  int percent = LERP_SCALE - ((this._Life * LERP_SCALE) / this.InitialLife); // 0 to 1024
  this.Transparency = this.TransparencyBegin + ((this.TransparencyDelta) * percent) / LERP_SCALE;
  this.Width = this.WidthBegin + ((this.WidthDelta) * percent) / LERP_SCALE;
  this.Height = this.HeightBegin + ((this.HeightDelta) * percent) / LERP_SCALE;
  if (this.SpriteDelta > 0)
  {
    this.Sprite
        = this.SpriteBegin
          + (this.SpriteCycleOffset + ((this.SpriteDelta) * percent) / LERP_SCALE)
                % this.SpriteDelta;
  }
  
#ifdef SCRIPT_API_v400
  this.Angle += this.RotationSpeed;
  if (this.Angle >= 360.0)
    this.Angle -= 360.0;
  if (this.Angle < 0.0)
    this.Angle += 360.0;
#endif

  int oidx = this._OverlayIdx;
  if (oidx >= 0 && overlayPool[oidx] != null)
  {
    Overlay *ovr = overlayPool[oidx];
    ovr.Transparency = this.Transparency;
    ovr.Graphic = this.Sprite;
#ifdef SCRIPT_API_v362
    ovr.SetPosition (this.X, this.Y, this.Width, this.Height);
#else
    ovr.X = this.X;
    ovr.Y = this.Y;
    ovr.Width = this.Width;
    ovr.Height = this.Height;
#endif
#ifdef SCRIPT_API_v400
    ovr.Rotation = this.Angle;
#endif
  }

  this._Life--;

  if (this.Y >= this.GroundY)
  {
    if (this.Bounces)
    {
      this.VelY = -(this.VelY * 700) / INT_FRAC; // Invert velocity and simulate energy loss
    }
    else
    {
      this._Life = 0; // Mark particle as dead (cheaper than other things...)
    }
  }
}

int get_Life(this Particle*)
{
  return this._Life;
}

void set_Life(this Particle*, int value)
{
  this._Life = value;
}

bool Particle::IsAlive ()
{
  return (this._Life > 0);
}

bool Particle::HitsPoint (int x, int y)
{
  return (x >= this.X) && (y >= this.Y)
        && (x <= this.X + this.Width) && (y <= this.Y + this.Height);
}

bool Particle::HitsRect (int x, int y, int width, int height)
{
  return (x + width >= this.X) && (y + height >= this.Y)
        && (x <= this.X + this.Width) && (y <= this.Y + this.Height);
}



// ---------------------- Emitter methods ----------------------------

void Emitter::SetPosition (int x, int y)
{
  this.X = x;
  this.Y = y;
}

void Emitter::SetParticleDefinitions (ParticleDefinition *defs[], int defCount)
{
  for (int i = 0; i < defCount; i++)
  {
    ParticleDefinition *def = defs[i];
    if (def.SpriteBegin == 0 && def.SpriteEnd == 0)
    {
      def.SpriteBegin = def.Sprite;
      def.SpriteEnd = def.Sprite;
    }
  }

  this.definitions = defs;
  this.definitionsCount = defCount;
}

// Initialize the emitter with position, particle definitions, and other parameters
void Emitter::Init (int x, int y, ParticleDefinition *defs[], int defCount, int emitAmount, int maxParticles)
{
  this.SetPosition (x, y);
  this.maxParticles = maxParticles;
  this.SetParticleDefinitions (defs, defCount);
  this.EmitAmount = emitAmount;
  this.particles = new Particle[maxParticles];
  for (int i = 0; i < maxParticles; i++)
  {
    this.particles[i] = new Particle;
  }
}

// returns TRUE if it emitted the particle SUCCESSFULLY
protected bool Emitter::_EmitParticleIndex (int i)
{
  Particle *p = this.particles[i];
  if (p.IsAlive ())
    return false;
    
  // Reuse dead particle if it's not alive anymore

  this.lastEmittedParticle = i; // remember to speed up loop for looking new particles
  int overlayIndex = FindFreeOverlaySlot ();
  if (overlayIndex >= 0)
  {
    // Randomly select a particle definition from the available definitions
    int defIndex = 0;
    if (this.definitionsCount > 0)
    {
      defIndex = Random (this.definitionsCount - 1);
    }
    ParticleDefinition *def = this.definitions[defIndex];

    if(p._OverlayIdx >= 0) {
      FreeOverlay(p._OverlayIdx);
    }
    if (overlayPool[overlayIndex] == null)
    {
      Overlay *ovr = Overlay.CreateGraphical (this.X, this.Y, def.Sprite);
      overlayPool[overlayIndex] = ovr;
    }
    p._Init (def, this.X, this.Y, overlayPool[overlayIndex]);
    p._OverlayIdx = overlayIndex;
    return true;
  }
  return false;
}

// Emit a single particle from the emitter, returns TRUE for FAILURE
protected bool Emitter::_EmitSingleParticle ()
{
  int i = (this.lastEmittedParticle + 1) % this.maxParticles;

  if (this._EmitParticleIndex (i)) // if it fail to emit, try to find a dead particle to reuse
    return false;

  int loop_at = i;
  for (; i < this.maxParticles; i++)
  {
    if (!this.particles[i].IsAlive ())
    {
      return !this._EmitParticleIndex (i);
    }
  }

  for (i = 0; i < loop_at; i++)
  {
    if (!this.particles[i].IsAlive ())
    {
      return !this._EmitParticleIndex (i);
    }
  }

  return true; // indicates something is wrong
}

// Emit particles from the emitter, returns true on success
bool Emitter::Emit ()
{
  for (int i = 0; i < this.EmitAmount; i++)
  {
    // if we either don't have more overlays or particles, return failure
    if (this._EmitSingleParticle ())
      return false;
  }
  return true;
}

// Update all particles
void Emitter::Update ()
{
  for (int i = 0; i < this.maxParticles; i++)
  {
    if (this.particles[i]._Life > 0)
    {
      this.particles[i]._Update ();
    }
    else if (this.particles[i]._OverlayIdx >= 0)
    {
      this.particles[i]._OverlayIdx = FreeOverlay (this.particles[i]._OverlayIdx);
    }
  }
}

Particle *[] Emitter::ParticlesHitPoint (int x, int y)
{
  int c[] = new int[this.maxParticles];
  int c_count;
  for (int i = 0; i < this.maxParticles; i++)
  {
    Particle *p = this.particles[i];
    if (p.IsAlive () && p.HitsPoint (x, y))
    {
      c[c_count] = i;
      c_count++;
    }
  }
  Particle *ps[] = new Particle[c_count + 1];
  for (int i = 0; i < c_count; i++)
  {
    ps[i] = this.particles[c[i]];
  }
  return ps;
}

Particle *[] Emitter::ParticlesHitRect (int x, int y, int width, int height) 
{
  int c[] = new int[this.maxParticles];
  int c_count = 0;
  for (int i = 0; i < this.maxParticles; i++)
  {
    Particle *p = this.particles[i];
    if (p.IsAlive () && p.HitsRect (x, y, width, height))
    {
      c[c_count] = i;
      c_count++;
    }
  }
  Particle *ps[] = new Particle[c_count + 1];
  for (int i = 0; i < c_count; i++)
  {
    ps[i] = this.particles[c[i]];
  }
  return ps;
}

void ContinuousEmitter::StartEmitting (int emitInterval)
{
  if (emitInterval < 4)
    emitInterval = 4;
  this.emitInterval = emitInterval;
  this.isEmitting = true;
}

void ContinuousEmitter::StopEmitting ()
{
  this.isEmitting = false;
}

void ContinuousEmitter::UpdateContinuous ()
{
  this.Update ();
  
  if (!this.isEmitting) return;
  
  this._emitCooldown--;
  if (this._emitCooldown <= 0)
  {
    bool success = this.Emit ();
    this._emitCooldown = this.emitInterval;
    
    // if we fail to emit we need to increase cooldown
    // we are emitting too many long lived particles per total particle count
    // TODO: test and actually evaluate if this does anything to help
    if(!success) // failed to emit,  increase interval
      this._emitCooldown+=2;
  }
}

// --- Global Particle Emitter for easy quick particle emission

ContinuousEmitter GPE;
export GPE;

void game_start ()
{
  ParticleDefinition *d[];
  int max_particles =  MAX_OVERLAYS; // (MAX_OVERLAYS * 4) / 5;
  int emit_count = max_particles / 16;
  GPE.Init (Screen.Width / 2, Screen.Height / 2, d, 0, emit_count,  max_particles);
}

void repeatedly_execute_always ()
{
  GPE.UpdateContinuous ();
}
[close]

I tried to hide more things in the script to not make the header too daunting and also to keep hidden some interfaces that may change in the future. There is still a lot of stuff leaking, most notably the Particle itself contains a ton of properties I would rather hide completely.

I think modifying the particles, other than their life, should be reserved to the Emitter - if at all. Anyway, if someone has any comments, ideas/suggestions in how to hide more stuff from the header, welcome any suggestions. Also if you catch a code comment in the header that is hard to understand, please tell me.

Edit: remembered the Overlays die when exiting a room, so need to do something to workaround this somehow. Essentially all emitters need to somehow clean their particles on room exit or some other strategy that enable going out of a room and then going back to not have invalid overlay indexes pointed by the particles.

I think I can't clear the particles of all emitters unless the Emitter is like a fake interface to a global emitter that exists behind the scenes. Other approach I can think is to keep both the current internal arrays to particles but also a global one and then I can clear all the particles individually after room fadeout event and then all the particles the emitters points to will be ok in a dead state, even though I don't have access to the emitters directly. Have no idea how to properly do this yet.
#256
@Giacomo , the link you shared is not for the game, it looks like some sheet music for some classical music, and I think there is one for Monkey Island (and One Piece?)  :-D
#257
@Giacomo  Can the game be configured to load and cause the error as soon as possible? If it can it would be nice to get a link to such build.

The callstack in the web build is a bit complex because there's some magic going on to handle the different game loops in AGS engine, so I would need to run it through a debugger to make some sense of it.
#258
Hey, if someone is looking for info on the sequel, I discovered @granulac has a blog with entries about it: https://meredithgran.com/tagged/games
#259
OK, just minor update of the code itself. Did some clean up, removed some code paths that weren't necessary (Threw in a few AbortGame calls and noticed some things never happened!). Other than this not much. Verified it works fine in AGS 3.6.1, 3.6.2 and 4.0, and in 4.0 it gets additional BlendModo and Rotation support for the particles.

I think I will move the properties to use PascalCase instead of the current camelCase, and use camelCase only in the function parameters and local variables.

Other than this trying to short the name of things when possible - switched from using collision to hit as one of the concepts, and also since the type ParticleDefinition is already there I can use only "def" to refer to it as a parameter.

If someone has some criticism on the code it would be nice to get it, I think I will try to encapsulate this all as a module and throw a simple demo together.

From top of my head the only feature I still wish to get in is some sort of deceleration, so that I could do something like the Splash particle but then have the particles still lay there for some time before disappearing.

module header
Particles.ash
Spoiler
Code: ags
// new module header

managed struct ParticleDefinition {
  int sprite;
  int offsetX; // Offset from the emitter position
  int offsetY; // Offset from the emitter position
  int life; // Lifetime of the particle
  int vx; // mili Velocity in x direction
  int vy; // mili Velocity in y direction
  int gravity; // mili Gravity effect on the particle
  int initialSprite;
  int finalSprite;
  int initialTransparency; // Initial transparency
  int finalTransparency; // Final transparency
  int initialWidth; // Initial width
  int finalWidth; // Final width
  int initialHeight; // Initial height
  int finalHeight; // Final height
  bool groundHitBounces;
  int groundY;
  #ifdef SCRIPT_API_v400
  BlendMode BlendMode;
  float rotationSpeed;
  float angle;
  #endif
};

managed struct Particle {
  int sprite;
  int x;
  int y;
  protected int mx; // mili x (~1000 times x)
  protected int my; // mili y (~1000 times y)
  int life;
  int initialLife;
  int overlayIndex; // This refers to the overlay in the overlay pool
  int vx; // x velocity
  int vy; // y velocity
  int gravity; // this is vertical acceleration downwards
  int transparency;
  int width;
  int height;
  int initialSprite;
  int rollSprite;
  int deltaSprite;
  int initialTransparency;
  int deltaTransparency;
  int initialWidth;
  int deltaWidth;
  int initialHeight;
  int deltaHeight;
  bool bounces;
  int groundY;
  #ifdef SCRIPT_API_v400
  float rotationSpeed;
  float angle;
  #endif
  /// returns true if particle is alive
  import bool IsAlive();
  /// returns true if particle rect overlaps point
  import bool HitsPoint(int x, int y);
  /// returns true if particle rect overlaps rect
  import bool HitsRect(int x, int y, int width, int height);
  // private stuff
  import void _Init(ParticleDefinition * def, int x, int y, Overlay* ovr); // $AUTOCOMPLETEIGNORE$
  import void _Update(); // $AUTOCOMPLETEIGNORE$
};

struct Emitter {
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition * definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);
  /// Get null terminated array of particles that overlaps with the point
  import Particle * [] ParticlesHitPoint(int x, int y);
  /// Get null terminated array of particles that overlaps with the rect
  import Particle * [] ParticlesHitRect(int x, int y, int width, int height);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition * defs[], int defCount, int emitCount = 10, int maxParticles = 50);
  /// Emit particles
  import void Emit();
  /// Update all particles
  import void Update();
  
  import protected bool EmitParticleIndex(int i);
  import protected bool EmitSingleParticle();
  protected int x;
  protected int y;
  protected int emitCount;
  protected int maxParticles;
  protected Particle * particles[]; // Pool of particles
  protected ParticleDefinition * definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  protected int lastEmittedParticle;
};

struct ContinuousEmitter extends Emitter {
  protected int emitRate;
  protected int _emitRateFrame;
  protected bool isEmitting;

  import void StartEmitting(int emitRate = 11);
  import void StopEmitting();
  import void UpdateContinuous();
};

/// Global Particle Emitter
import ContinuousEmitter GPE;
[close]

module script
Particles.asc
Spoiler
Code: ags
// new module script

#define MAX_LERP 1024
#define INT_FRAC 1024
#define MAX_OVERLAYS 2048
Overlay *overlayPool[MAX_OVERLAYS];
bool overlayUsed[MAX_OVERLAYS];
int lastUsed;

int InvalidateOverlay (int index)
{
  if (overlayPool[index] != null)
  {
    overlayPool[index].Transparency = 100;
  }
  overlayUsed[index] = false; // Mark the overlay slot as free
  return -1;
}

// Find an available overlay slot in the pool
int GetAvailableOverlayIndex ()
{
  for (int i = lastUsed; i < MAX_OVERLAYS; i++)
  {
    if (!overlayUsed[i])
    {
      overlayUsed[i] = true;
      lastUsed = i;
      return i;
    }
  }
  for (int i = 0; i < lastUsed; i++)
  {
    if (!overlayUsed[i])
    {
      overlayUsed[i] = true;
      lastUsed = i;
      return i;
    }
  }
  return -1; // No available overlay
}

void UpdateOverlayFromParticle (Overlay *ovr, Particle *p)
{
  ovr.Transparency = p.transparency;
  ovr.Graphic = p.sprite;
#ifdef SCRIPT_API_v362
  ovr.SetPosition (p.x, p.y, p.width, p.height);
#else
  ovr.X = p.x;
  ovr.Y = p.y;
  ovr.Width = p.width;
  ovr.Height = p.height;
#endif
#ifdef SCRIPT_API_v400
  ovr.Rotation = p.angle;
#endif
}

// ---------------------- Particle methods ----------------------------

bool Particle::IsAlive ()
{
  return (this.life > 0);
}

bool Particle::HitsPoint (int x, int y)
{
  return (x >= this.x) && (y >= this.y)
         && (x <= this.x + this.width) && (y <= this.y + this.height);
}

bool Particle::HitsRect (int x, int y, int width, int height)
{
  return (x + width >= this.x) && (y + height >= this.y)
         && (x <= this.x + this.width) && (y <= this.y + this.height);
}

void Particle::_Init (ParticleDefinition *def, int x, int y, Overlay* ovr)
{
  this.mx = INT_FRAC * (x + def.offsetX); // Offset based on emitter
  this.my = INT_FRAC * (y + def.offsetY); // Offset based on emitter
  this.x = this.mx / INT_FRAC;
  this.y = this.my / INT_FRAC;
  this.life = def.life;
  this.vx = (def.vx * INT_FRAC) / 1000;
  this.vy = (def.vy * INT_FRAC) / 1000;
  this.gravity = (def.gravity * INT_FRAC) / 1000;

  this.transparency = def.initialTransparency;
  this.initialTransparency = def.initialTransparency;
  this.deltaTransparency = def.finalTransparency - def.initialTransparency;

  this.width = def.initialWidth;
  this.initialWidth = def.initialWidth;
  this.deltaWidth = def.finalWidth - def.initialWidth;

  this.height = def.initialHeight;
  this.initialHeight = def.initialHeight;
  this.deltaHeight = def.finalHeight - def.initialHeight;

  this.rollSprite = def.sprite - def.initialSprite;
  this.sprite = def.sprite;
  this.initialSprite = def.initialSprite;
  this.deltaSprite = def.finalSprite - def.initialSprite;
  
#ifdef SCRIPT_API_v400
  this.rotationSpeed = def.rotationSpeed;
  this.angle = def.angle;
#endif

  this.initialLife = def.life; // Store initial life for transitions
  this.bounces = def.groundHitBounces;
  this.groundY = def.groundY;
  if (this.groundY <= 0)
  {
    this.groundY = 16777216; // a big number so it is not reached
  }
  
#ifdef SCRIPT_API_v400
  ovr.BlendMode = def.BlendMode;
#endif
  UpdateOverlayFromParticle (ovr, this);
}

// Update the particle state and sync with overlay
void Particle::_Update ()
{
  // alive check is done before calling this function
  this.mx += this.vx;
  this.my += this.vy;
  this.vy += this.gravity; // Apply gravity
  this.x = this.mx / INT_FRAC;
  this.y = this.my / INT_FRAC;

  // Calculate the scaling and transparency transitions based on life
  int percent = MAX_LERP - ((this.life * MAX_LERP) / this.initialLife); // 0 to 1024
  this.transparency = this.initialTransparency + ((this.deltaTransparency) * percent) / MAX_LERP;
  this.width = this.initialWidth + ((this.deltaWidth) * percent) / MAX_LERP;
  this.height = this.initialHeight + ((this.deltaHeight) * percent) / MAX_LERP;
  if (this.deltaSprite > 0)
  {
    this.sprite
        = this.initialSprite
          + (this.rollSprite + ((this.deltaSprite) * percent) / MAX_LERP)
                % this.deltaSprite;
  }
  
#ifdef SCRIPT_API_v400
  this.angle += this.rotationSpeed;
  if (this.angle >= 360.0)
    this.angle -= 360.0;
  if (this.angle < 0.0)
    this.angle += 360.0;
#endif

  int oidx = this.overlayIndex;
  if (oidx >= 0 && overlayPool[oidx] != null)
  {
    Overlay *ovr = overlayPool[oidx];
    ovr.Transparency = this.transparency;
    ovr.Graphic = this.sprite;
#ifdef SCRIPT_API_v362
    ovr.SetPosition (this.x, this.y, this.width, this.height);
#else
    ovr.X = this.x;
    ovr.Y = this.y;
    ovr.Width = this.width;
    ovr.Height = this.height;
#endif
#ifdef SCRIPT_API_v400
    ovr.Rotation = this.angle;
#endif
  }

  this.life--;

  if (this.y >= this.groundY)
  {
    if (this.bounces)
    {
      this.vy = -(this.vy * 700) / INT_FRAC; // Invert velocity and simulate energy loss
    }
    else
    {
      this.life = 0; // Mark particle as dead (cheaper than other things...)
    }
  }
}


// ---------------------- Emitter methods ----------------------------

void Emitter::SetPosition (int x, int y)
{
  this.x = x;
  this.y = y;
}

void Emitter::SetParticleDefinitions (ParticleDefinition *defs[], int defCount)
{
  for (int i = 0; i < defCount; i++)
  {
    ParticleDefinition *def = defs[i];
    if (def.initialSprite == 0 && def.finalSprite == 0)
    {
      def.initialSprite = def.sprite;
      def.finalSprite = def.sprite;
    }
  }

  this.definitions = defs;
  this.definitionsCount = defCount;
}

// Initialize the emitter with position, particle definitions, and other parameters
void Emitter::Init (int x, int y, ParticleDefinition *defs[], int defCount, int emitCount, int maxParticles)
{
  this.SetPosition (x, y);
  this.maxParticles = maxParticles;
  this.SetParticleDefinitions (defs, defCount);
  this.emitCount = emitCount;
  this.particles = new Particle[maxParticles];
  for (int i = 0; i < maxParticles; i++)
  {
    this.particles[i] = new Particle;
  }
}

// returns TRUE if it emitted the particle SUCCESSFULLY
protected bool Emitter::EmitParticleIndex (int i)
{
  Particle *p = this.particles[i];
  if (p.IsAlive ())
    return false;

  this.lastEmittedParticle = i;

  // Reuse dead particle if it's not alive anymore
  int overlayIndex = GetAvailableOverlayIndex ();
  if (overlayIndex >= 0)
  {
    // Randomly select a particle definition from the available definitions
    int defIndex = 0;
    if (this.definitionsCount > 0)
    {
      defIndex = Random (this.definitionsCount - 1);
    }
    ParticleDefinition *def = this.definitions[defIndex];

    if (overlayPool[overlayIndex] == null)
    {
      Overlay *ovr = Overlay.CreateGraphical (this.x, this.y, def.sprite);
      overlayPool[overlayIndex] = ovr;
    }
    p._Init (def, this.x, this.y, overlayPool[overlayIndex]);
    p.overlayIndex = overlayIndex;
    return true;
  }
  return false;
}

// Emit a single particle from the emitter, returns TRUE for FAILURE
protected bool Emitter::EmitSingleParticle ()
{
  int i = (this.lastEmittedParticle + 1) % this.maxParticles;

  if (this.EmitParticleIndex (i)) // if it fail to emit, try to find a dead particle to reuse
    return false;

  int loop_at = i;
  for (; i < this.maxParticles; i++)
  {
    if (!this.particles[i].IsAlive ())
    {
      return !this.EmitParticleIndex (i);
    }
  }

  for (i = 0; i < loop_at; i++)
  {
    if (!this.particles[i].IsAlive ())
    {
      return !this.EmitParticleIndex (i);
    }
  }

  // FIX-ME: if we get here we need to do some sort of cooldown
  // the issue is we are emitting too many particles per total particle count
  // along with particles that have a too big life
  return true; // indicates something is wrong
}

// Emit particles from the emitter
void Emitter::Emit ()
{
  for (int i = 0; i < this.emitCount; i++)
  {
    if (this.EmitSingleParticle ())
      return;
  }
}

// Update all particles
void Emitter::Update ()
{
  for (int i = 0; i < this.maxParticles; i++)
  {
    if (this.particles[i].life > 0)
    {
      this.particles[i]._Update ();
    }
    else if (this.particles[i].overlayIndex >= 0)
    {
      this.particles[i].overlayIndex
          = InvalidateOverlay (this.particles[i].overlayIndex);
    }
  }
}

Particle *[] Emitter::ParticlesHitPoint (int x, int y)
{
  int c[] = new int[this.maxParticles];
  int c_count;
  for (int i = 0; i < this.maxParticles; i++)
  {
    Particle *p = this.particles[i];
    if (p.IsAlive () && p.HitsPoint (x, y))
    {
      c[c_count] = i;
      c_count++;
    }
  }
  Particle *ps[] = new Particle[c_count + 1];
  for (int i = 0; i < c_count; i++)
  {
    ps[i] = this.particles[c[i]];
  }
  return ps;
}

Particle *[] Emitter::ParticlesHitRect (int x, int y, int width, int height) 
{
  int c[] = new int[this.maxParticles];
  int c_count = 0;
  for (int i = 0; i < this.maxParticles; i++)
  {
    Particle *p = this.particles[i];
    if (p.IsAlive () && p.HitsRect (x, y, width, height))
    {
      c[c_count] = i;
      c_count++;
    }
  }
  Particle *ps[] = new Particle[c_count + 1];
  for (int i = 0; i < c_count; i++)
  {
    ps[i] = this.particles[c[i]];
  }
  return ps;
}

void ContinuousEmitter::StartEmitting (int emitRate)
{
  if (emitRate < 4)
    emitRate = 4;
  this.emitRate = emitRate;
  this.isEmitting = true;
}

void ContinuousEmitter::StopEmitting ()
{
  this.isEmitting = false;
}

void ContinuousEmitter::UpdateContinuous ()
{
  this.Update ();
  if (this.isEmitting)
  {
    this._emitRateFrame--;
    if (this._emitRateFrame <= 0)
    {
      this._emitRateFrame = this.emitRate;
      this.Emit ();
    }
  }
}

// --- Global Particle Emitter for easy quick particle emission

ContinuousEmitter GPE;
export GPE;

void game_start ()
{
  ParticleDefinition *d[];
  int max_particles =  MAX_OVERLAYS; // (MAX_OVERLAYS * 4) / 5;
  int emit_count = max_particles / 16;
  GPE.Init (Screen.Width / 2, Screen.Height / 2, d, 0, emit_count,  max_particles);
}

void repeatedly_execute_always ()
{
  GPE.UpdateContinuous ();
}
[close]

Edit: reading my code again, made a few more notes about it

Spoiler
A few things to rename

emitRate, replace with emitInterval
InvalidateOverlay, replace with FreeOverlay
GetAvailableOverlayIndex, replace with FindFreeOverlaySlot
UpdateOverlayFromParticle, replace with SyncParticleOverlay
These should help a little bit with making things easier to understand. Need to read the code a bit more to see if there are more opportunities for renaming things.

Oh, probably also better to rename initialX and finalX to instead be xStart and xEnd, as it may work better in the autocomplete. Maybe xBegin and xEnd, sounds worse but makes the initial state show before in the autocomplete.

Reading again, lastEmittedParticle is a bit long and we only emit particles, and that is an index, so maybe just only lastEmitted or lastEmittedIndex, that it's a particle is implied by the emitted.

Ah, right, similarly SetParticleDefinitions can be simply SetDefinitions since we don't support other type of definition other than particles
[close]

Overall I few I am approaching to have some version 0.1.0 that could make into release, I think I will do the changes I thought, write up some docs, make up a few demo rooms and see how it goes.
#260
Which browser are you using? Can you see anything else on the console? You can set the acsetup.cfg to have ags spit more log information. I think it's just

Code: ags
[log]
stdout = all:all

To get literally everything, and then you can put the log somewhere we can read - when you read it hopefully will give information on where things are just before the crash.

Other than this the way I usually go with my code to figure the origin of the problem is to remove things in large areas to get the game to go back working quickly and slowly add things back until something breaks, and then I remove the last thing to verify if it works again, and so this helps me isolate the issue.

And regarding the code, please use pointers and when possible instead of hardcoding integers as IDs of anything, those a bound to have human mistakes.
SMF spam blocked by CleanTalk