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

#261
Just commenting to say that I am reading it along. Interesting stuff, please keep writing! :)
#262
I think this one is new

https://lu608b.itch.io/in-black-city

I see a few Display being used, so it looks like someone's first AGS game, haven't actually played yet - just quickly ran to check the basics.
#263
Engine Development / Re: AGS engine Linux port
Fri 18/10/2024 16:13:47
Just wanted to give a small note here for Gamedevs using Steam, it appears there is a new pane in Steamworks, since docs haven't been updated I will just explicitly write about it here

Steamworks -> Apps & Packages -> Select your game -> Installation -> Linux Runtime (this is new!)
Now you can set your mapping between the Linux Runtime and the branch of your App

The Linux Runtime is a container (like?) environment where the Linux game runs and one thing it does is it enable you to compile your game in a container and then have the game run in the same container in the user machine. AGS Linux binaries are built using a very old Debian Jessie container, for both 32-bit and 64-bit builds.

By using the containers you could have AGS (and plug-ins) built using a newer compiler and having access to newer libraries.

https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/blob/main/docs/container-runtime.md

This lowers pressure in AGS side to keep compatibility with really old debian, I think, but this also requires the Gamedevs to pay attention to how these runtimes work.

The default if you as a dev do nothing is that Steam will use Scout, which is the Ubuntu 12.04 compatible container - it is compatible with the current Linux binaries we deliver so there is no need to rush to do anything right now. It is a twelve years old distro though, so we can think later about upgrading to something newer that has compatibility with a newer container - but it would require Gamedevs to configure this on release since it seems Valve isn't going to change the default soon.
#264
Snarky's detailed answer is the best step by step approach and definitely the way to go.

I am just going to mention two things to give options.

If you have a custom say for all your characters, you throw an if clause there in your custom say to check if it's the character specific and then use the character specific font.

Another approach if you use my fancy module is to just insert the font tag in the text strings directly. This is a lot more trouble, but just mentioning because it is a possibility.
#265
Something like that can be an approach, but I think for now I will leave these for the user to figure it out.

Did one more addition here with "collision", added a GetParticlesCollidingPoint and GetParticlesCollidingRect, which can test the particle with is always a Rect (AABB, defined by x, y, width and height) with something else. I will probably also add a circle later too as the target to test collision with.


The collision is just an overlap test, and I made the example in the room in the ParticlePlayerCollision function, which what it does is cause the life of the particle to be reduced when in collision with the player.


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;
};

managed struct Particle {
  int sprite;
  int x;
  int y;
  int mx;
  int my;
  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;

  // Initialize the particle with its position, life, velocity, and transparency
  import void Init(ParticleDefinition* def, int x, int y, int overlayIndex);
  import void Update(); // Update particle position and overlay
  import bool IsAlive(); // Check if particle is still alive
  import bool IsCollidingPoint(int x, int y);
  import bool IsCollidingRect(int x, int y, int width, int height);
};

struct Emitter {
  protected int x;
  protected int y;
  protected int emitParticleCount;
  protected int particleCount;
  protected Particle* particles[]; // Pool of particles
  protected ParticleDefinition* definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  protected int lastEmittedParticle;
  
  /// 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 have collision with the point
  import Particle* [] GetParticlesCollidingPoint(int x, int y);
  
  import Particle* [] GetParticlesCollidingRect(int x, int y, int width, int height);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount = 10, int particleCount = 50);
  /// Emit a specific particle
  import protected bool EmitParticleIndex(int i);
  /// Emit a single particle
  import protected bool EmitSingleParticle();
  /// Emit particles
  import void Emit();
  /// Update all particles
  import void Update();
};

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]

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

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

#define MAX_LERP 1024
#define INT_FRAC 1024

// lerp from percent 0 to 1024
int _Lerp(int start, int end, int percent) {
  if (percent < 0) percent = 0;
  if (percent > MAX_LERP) percent = MAX_LERP;
  return start + ((end - start) * percent) / MAX_LERP;
}

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
function 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
}

void Particle::Init(ParticleDefinition* def, int x, int y,  int overlayIndex) {
  if(this.overlayIndex >= 0 && this.overlayIndex < MAX_OVERLAYS) {
    InvalidateOverlay(this.overlayIndex);
  }

  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;
  
  this.overlayIndex = overlayIndex;
  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
  }

  if (overlayIndex >= 0 && overlayPool[overlayIndex] != null) {
    UpdateOverlayFromParticle(overlayPool[overlayIndex], this);
  }
}

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

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

// TODO: check if this correct
bool Particle::IsCollidingRect(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);
}

// 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;
  }
  
  int oidx = this.overlayIndex;
  if (oidx >= 0 && overlayPool[oidx] != null) {
    // UpdateOverlayFromParticle(overlayPool[this.overlayIndex], this);
    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
  }
  
  this.life--;
  
  if (this.y >= this.groundY) {
    if (this.bounces) {
      this.vy = -(this.vy * 700)/INT_FRAC; // Invert velocity, reduce it to simulate energy loss
    } else {
      this.life = 0; // Mark particle as dead (cheaper than other things...)
    }
  }
}

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

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

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

protected bool Emitter::EmitParticleIndex(int i)
{
  if(this.particles[i].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;
    }
    this.particles[i].Init(def, this.x, this.y, overlayIndex);
  }
  return true;
}

// Emit a single particle from the emitter
protected bool Emitter::EmitSingleParticle() {
  //System.Log(eLogInfo, ">>begin sp");
  int i = (this.lastEmittedParticle + 1) % this.particleCount;
  
  if(this.EmitParticleIndex(i))
    return false;
  
  //System.Log(eLogInfo, "loop1 sp");
  int loop_at = i;
  for (; i < this.particleCount; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  //System.Log(eLogInfo, "loop2 sp");
  for (i=0; i < loop_at; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  // 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.emitParticleCount; i++) {
    if(this.EmitSingleParticle())
      return;
  }
}

// Update all particles
void Emitter::Update() {
  for (int i = 0; i < this.particleCount; 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::GetParticlesCollidingPoint(int x, int y)
{
  int c[] = new int[this.particleCount];
  int c_count;
  for (int i = 0; i < this.particleCount; i++) {
    Particle* p = this.particles[i];
    if(p.IsAlive() && p.IsCollidingPoint(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::GetParticlesCollidingRect(int x, int y, int width, int height)
{  
  int c[] = new int[this.particleCount];
  int c_count = 0;
  for (int i = 0; i < this.particleCount; i++) {
    Particle* p = this.particles[i];
    if(p.IsAlive() && p.IsCollidingRect(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();
    }
  } 
}
  
ContinuousEmitter GPE;
export GPE;

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

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

example room
room1.asc
Spoiler
Code: ags
// room script file

//Emitter emt;

function hGlowingOrb_Look(Hotspot *thisHotspot, CursorMode mode)
{
  player.Say("It is the second best glowing orb that I've seen today.");
}

ParticleDefinition* GetFireworksParticle()
{
  ParticleDefinition* fireworksParticle = new ParticleDefinition;
  fireworksParticle.life = 40;
  // make the velocities circular
  float r = 2000.0*Maths.Sqrt(IntToFloat(Random(8192))/8192.0);
  float theta =  2.0 * Maths.Pi*(IntToFloat(Random(8192))/8192.0);
  fireworksParticle.vx = FloatToInt(r * Maths.Cos(theta)); // Random outward velocity
  fireworksParticle.vy = FloatToInt(r * Maths.Sin(theta));
  
  fireworksParticle.gravity = 0; // No gravity
  fireworksParticle.initialTransparency = 0;
  fireworksParticle.finalTransparency = 100;
  fireworksParticle.initialWidth = 2;
  fireworksParticle.finalWidth = 20; // Expanding outward
  fireworksParticle.initialHeight = 2;
  fireworksParticle.finalHeight = 20;
  return fireworksParticle;
}

ParticleDefinition* GetSparkleParticle()
{
  ParticleDefinition* sparkleParticle = new ParticleDefinition;
  sparkleParticle.life = 50;
  sparkleParticle.vx = Random(3000) - 1000;
  sparkleParticle.vy = Random(3000) - 1000;
  sparkleParticle.initialTransparency = 0;
  sparkleParticle.finalTransparency = 100;
  sparkleParticle.initialWidth = 3;
  sparkleParticle.finalWidth = 8;
  sparkleParticle.initialHeight = 3;
  sparkleParticle.finalHeight = 8;
  sparkleParticle.gravity = 100;
  sparkleParticle.groundY = 154;
  sparkleParticle.groundHitBounces = true;
  return sparkleParticle;
}

ParticleDefinition* GetExplosionParticle()
{
  ParticleDefinition* explosionParticle = new ParticleDefinition;  
  explosionParticle.sprite = 1+ Random(60);
  explosionParticle.initialSprite = 1;
  explosionParticle.finalSprite = 61;  
  explosionParticle.life = 40;
  explosionParticle.vx = Random(3000) - 1500;
  explosionParticle.vy = Random(3000) - 1500;
  explosionParticle.gravity =  -90;
  explosionParticle.initialTransparency = 15;
  explosionParticle.finalTransparency = 100;
  explosionParticle.initialWidth = 12;
  explosionParticle.finalWidth = 50;
  explosionParticle.initialHeight = 12;
  explosionParticle.finalHeight = 50;
  return explosionParticle;
}

ParticleDefinition* GetSmokeParticle()
{
  ParticleDefinition* smokeParticle = new ParticleDefinition;
  smokeParticle.life = 40+Random(14);
  smokeParticle.vy = -1000-Random(1000);
  smokeParticle.initialTransparency = 0;
  smokeParticle.finalTransparency = 100;
  smokeParticle.initialWidth = 10+Random(2);
  smokeParticle.finalWidth = 20+Random(2);
  smokeParticle.initialHeight = 20+Random(2);
  smokeParticle.finalHeight = 10+Random(2);
  return smokeParticle;
}

ParticleDefinition* GetBubbleParticle()
{
  ParticleDefinition* bubbleParticle = new ParticleDefinition;
  bubbleParticle.life = 60;
  bubbleParticle.vx = Random(500) - 250; // Small horizontal drift
  bubbleParticle.vy = -1000 - Random(500); // Rising upwards
  bubbleParticle.gravity = -200; // Rising effect
  bubbleParticle.initialTransparency = 30;
  bubbleParticle.finalTransparency = 100;
  bubbleParticle.initialWidth = 5;
  bubbleParticle.finalWidth = 15; // Expands as it rises
  bubbleParticle.initialHeight = 5;
  bubbleParticle.finalHeight = 15;
  return bubbleParticle;
}

ParticleDefinition* GetRainParticle()
{
  ParticleDefinition* rainParticle = new ParticleDefinition;
  rainParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  rainParticle.offsetY = -Random(30);
  rainParticle.life = 50;
  rainParticle.vx = Random(500) - 250; // Slight horizontal movement
  rainParticle.vy = 3000; // Falling down quickly
  rainParticle.gravity = 180; // Light gravity effect
  rainParticle.initialTransparency = 30;
  rainParticle.finalTransparency = 80;
  rainParticle.initialWidth = 2;
  rainParticle.finalWidth = 2;
  rainParticle.initialHeight = 10;
  rainParticle.finalHeight = 15; // Lengthening as it falls
  return rainParticle;
}

ParticleDefinition* GetFireParticle()
{
  ParticleDefinition* fireParticle = new ParticleDefinition;
  fireParticle.life = 35;
  fireParticle.vx = Random(1000) - 500; // Small horizontal variance
  fireParticle.vy = -1200 - Random(500); // Rising upward
  fireParticle.gravity = -50; // Slow upward pull
  fireParticle.initialTransparency = 50;
  fireParticle.finalTransparency = 100; // Disappears as it rises
  fireParticle.initialWidth = 10;
  fireParticle.finalWidth = 20; // Expands as it rises
  fireParticle.initialHeight = 10;
  fireParticle.finalHeight = 15;
  return fireParticle;
}

ParticleDefinition* GetSnowParticle()
{
  ParticleDefinition* snowParticle = new ParticleDefinition;
  snowParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  snowParticle.offsetY = -Random(30);
  snowParticle.life = 160;
  snowParticle.vx = Random(300) - 150; // Slight horizontal drift
  snowParticle.vy = Random(300) + 220; // Slow downward movement
  snowParticle.gravity = 10; // Minimal gravity effect
  snowParticle.initialTransparency = 50;
  snowParticle.finalTransparency = 75;
  snowParticle.initialWidth = 4;
  snowParticle.finalWidth = 6; // Slight expansion as it falls
  snowParticle.initialHeight = 4;
  snowParticle.finalHeight = 6;
  return snowParticle;
}

ParticleDefinition* GetSplashParticle()
{
  ParticleDefinition* p = new ParticleDefinition;
  p.life = 96;  
  // squished circle
  float r = 2000.0*Maths.Sqrt(IntToFloat(Random(8192))/8192.0);
  float theta =  2.0 * Maths.Pi*(IntToFloat(Random(8192))/8192.0);
  p.vx = FloatToInt(r * Maths.Cos(theta))/3;
  p.vy = FloatToInt(r * Maths.Sin(theta))/2-2000; // vy shots upwards
  p.initialTransparency = 20;
  p.finalTransparency = 100;
  p.initialWidth = 2;
  p.finalWidth = 8;
  p.initialHeight = 8;
  p.finalHeight = 2;
  p.gravity = 100;
  p.groundY = 150 + Random(9);
  p.groundHitBounces = true;
  return p;
}

enum PresetParticleType {
  ePPT_Fireworks, 
  ePPT_Sparkle, 
  ePPT_Explosion, 
  ePPT_Smoke, 
  ePPT_Bubble, 
  ePPT_Rain, 
  ePPT_Fire, 
  ePPT_Snow, 
  ePPT_Splash
};

#define ePPT_Last ePPT_Splash

ParticleDefinition* [] GetParticleDefinitionsArrayByType(PresetParticleType type, int count)
{
  ParticleDefinition* definitions[] = new ParticleDefinition[count];
  int i;
  switch(type) {
    case ePPT_Fireworks:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireworksParticle();
      }
    break;
    case ePPT_Sparkle:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSparkleParticle();
      }
    break;
    case ePPT_Explosion:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetExplosionParticle();
      }
    break;
    case ePPT_Smoke:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSmokeParticle();
      }
    break;
    case ePPT_Bubble:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetBubbleParticle();
      }
    break;
    case ePPT_Rain:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetRainParticle();
      }
    break;
    case ePPT_Fire:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireParticle();
      }
    break;
    case ePPT_Snow:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSnowParticle();
      }
    break;
    case ePPT_Splash:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSplashParticle();
      }
    break;
  }  
  return definitions;
}

String GetTypeName(PresetParticleType type) {  
  switch(type) {
    case ePPT_Fireworks:
      return "Fireworks";
    case ePPT_Sparkle:
      return "Sparkle";
    case ePPT_Explosion:
      return "Explosion";
    case ePPT_Smoke:
      return "Smoke";
    case ePPT_Bubble:
      return "Bubble";
    case ePPT_Rain:
      return "Rain";
    case ePPT_Fire:
      return "Fire";
    case ePPT_Snow:
      return "Snow";
    case ePPT_Splash:
      return "Splash";
    default:
      return "Unknown";
  }  
}

void SetEmitterToType(PresetParticleType type)
{
  int definitions_count = 2048;
  ParticleDefinition* definitions[] = GetParticleDefinitionsArrayByType(type, definitions_count);
  GPE.SetParticleDefinitions(definitions, definitions_count);
  lbl_particle_selected.Text = GetTypeName(type);
  
  if(type == ePPT_Rain || type == ePPT_Snow) {
    GPE.SetPosition(Screen.Width/2, 0);
    GPE.StartEmitting();
  } else {
    GPE.StopEmitting();
  }
}

void ParticlePlayerCollision()
{  
  ViewFrame* c_vf = Game.GetViewFrame(player.NormalView, 0, 0);
  float scaling = IntToFloat(player.Scaling) / 100.00;
  int p_rect_width = FloatToInt(IntToFloat(Game.SpriteWidth[c_vf.Graphic]) * scaling);
  int p_rect_height = FloatToInt(IntToFloat(Game.SpriteHeight[c_vf.Graphic]) * scaling);
  int p_rect_x = player.x - p_rect_width/2;
  int p_rect_y = player.y - p_rect_height;
  
  //DrawingSurface* surf = Room.GetDrawingSurfaceForBackground();
  //surf.DrawingColor = Random(65535);
  //surf.DrawRectangle(p_rect_x, p_rect_y, p_rect_x + p_rect_width, p_rect_y + p_rect_height);
  //surf.Release();
  
  Particle* pcp [] = GPE.GetParticlesCollidingRect(p_rect_x, p_rect_y, p_rect_width, p_rect_height);
  for(int i=0; pcp[i] != null; i++)
    pcp[i].life-=2;
}

int particle_type;
void on_call (int value)
{
  if(value == 1) {
    particle_type++;
    if(particle_type> ePPT_Last) {
      particle_type = 1;
    }
  }
  SetEmitterToType(particle_type);
}

function room_Load()
{  
  particle_type = ePPT_Splash;
  SetEmitterToType(particle_type);
}

void on_mouse_click(MouseButton button)
{
  if(particle_type == ePPT_Rain || particle_type == ePPT_Snow)
    return;
  GPE.SetPosition(mouse.x, mouse.y);
  GPE.Emit();
}

int mb_press;
function room_RepExec()
{
  ParticlePlayerCollision();
  
  if(particle_type == ePPT_Rain || particle_type == ePPT_Snow)
    return;
    
  if(mouse.IsButtonDown(eMouseLeft))
    mb_press++;
  else
    mb_press = 0;
  
  if(mb_press > 10) {
    GPE.SetPosition(mouse.x, mouse.y);
    GPE.StartEmitting(5);
  } else {
    GPE.StopEmitting();
  }
}

function room_AfterFadeIn()
{

}
[close]
#266
There is a proposal from CW here: https://github.com/adventuregamestudio/ags/pull/2489

I think it's intended for 3.6.2. and you would be interested to take a look because it would be relevant for already released games.

Quote from: Dave Gilbert on Wed 16/10/2024 15:35:13The demo version of the game was set to put its savefiles in the same folder as the main game.

For this specific one time thing, isn't possible to put a new dir name for the new release? Something like "WadjetEye.OldSkies", it would even help to sort save dirs by name and seeing all Wadjet Eye games. Or leave everyone confused and name the dir TheOldSkies.

Ah, another alternative would be to do something in Steam itself, like run a script that checks if OldSkiesDemo dir exists or not and if not, if the OldSkies dir exists, rename it to OldSkiesDemo. Needs to written separately per platform the game exists. This would make the saves disappear to a different did if the person has not run OldSkies demo, and run it, uninstall and reinstall it. I don't know how this would work with the Cloud Saves though, I would ask @Dualnames to test this if it's possible.
#267
I had an idea to pass the overlays to be "owned" by the emitter, this would take out one for loop out because I currently need to find an available overlay from the pool everytime I try to initialize a particle, the issue though is I need the overlay at the Update loop, so the way I thought to solve that is having the particle update take one parameter with the overlay so that the emitter can pass it down.

Other idea is have an API in the emitter to retrieve a null terminated array of particles that are colliding with a specific point (going by AABB collision test).

What I can't figure it out is if the ergonomics of using particle definition is good or bad, I like it a lot but I don't know if people will have a bad time figuring it out, and I can't figure any helper functions to make it easier to generate them.
#269
Ah, that should be possible already @RootBound , the way it would work is similar to the sparks but having the velocity y be more negative so it goes upwards, the sparks distribution is very simple and some particles do go upwards, but just a little, and you have to set gravity (downwards acceleration) in a way it would push down.

I want to try later to add subparticles, so it would be possible to have the splashing be triggered by the rain particle hitting the ground, but I still need to think a bit on how to do it.

The trick is always how to get the most interesting/fun/diverse effects with the least amount of code running, and a lot of care has to be put in the particle update function so it can still perform well.

Edit: ok, figured the ergonomics of gravity well require that the well be in the emitter and not in the particle itself, if I want to have the player have such gravity - think of being able to atract coin particles as you walk near them.

Edit2: also found a fix to ensure the fireworks goes outward in a way it expands as a circle

Code: ags
ParticleDefinition* GetFireworksParticle()
{
  ParticleDefinition* fireworksParticle = new ParticleDefinition;
  fireworksParticle.life = 40;
  // make the velocities circular <<-- this is the new thing!
  float r = 2000.0*Maths.Sqrt(IntToFloat(Random(8192))/8192.0);
  float theta =  2.0 * Maths.Pi*(IntToFloat(Random(8192))/8192.0);
  fireworksParticle.vx = FloatToInt(r * Maths.Cos(theta)); // Random outward velocity
  fireworksParticle.vy = FloatToInt(r * Maths.Sin(theta));
  
  fireworksParticle.gravity = 0; // No gravity
  fireworksParticle.initialTransparency = 0;
  fireworksParticle.finalTransparency = 100;
  fireworksParticle.initialWidth = 2;
  fireworksParticle.finalWidth = 20; // Expanding outward
  fireworksParticle.initialHeight = 2;
  fireworksParticle.finalHeight = 20;
  return fireworksParticle;
}

I had previously had to figure this proper way of sampling a circle in this other post here!
#270
Uhm, it depends, not sure on the splash meaning, if it's going up and then down, you make some upwards initial velocity set in the particle and additionally give it some "gravity" (acceleration downwards) that can win from such velocity and it should present such movement.

There's a way to fake this too using some easing specific function, for now I haven't added such thing because I am not convinced that the performance payment is worth it, I would need to extend the particle and emitter for something specifically for handling this in a way that it doesn't affect the performance of the "non-extra eased" interpolation, need to think about it.

Other possibility is having a particle emit another particle when they die. So a drop of water could fall, reach ground (particle gets life set to 0 at this point), and then emit particles. I could not think in some proper way to do this in ags3, in ags4 it's easy because I can have a managed struct hold pointers to its own type, so the particle definition could just hold an array of particle definitions, and then it would be easy to script such systems. To do it in ags3 I would need to linearize this somehow in the same array but the ergonomics are not good. I guess I may add this feature behind a version check for ags4.

Edit:

The big idea in this module is that the emitter holds an array of particle definitions, that describes the intended behavior of a particular particle and the emitter randomly samples and chooses one of these when emitting.

The idea here is that you can great a big number of definitions that may have each property created with whatever probability distribution you want at creation time and the systems is made so you don't pay for this randomness at emit time and neither require the particle definition to have many different properties to describe such randomness. It's expected that the small sample of particles that get emitted hold a similar distribution of all your particle definitions.

Edit2: did an upgrade adding sprite interpolation

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;
};

managed struct Particle {
  int sprite;
  int x;
  int 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;

  // Initialize the particle with its position, life, velocity, and transparency
  import void Init(ParticleDefinition* def, int x, int y, int overlayIndex);
  import void Update(); // Update particle position and overlay
  import bool IsAlive(); // Check if particle is still alive
};

struct Emitter {
  protected int x;
  protected int y;
  protected int emitParticleCount;
  protected int particleCount;
  protected Particle* particles[]; // Pool of particles
  protected ParticleDefinition* definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  protected int lastEmittedParticle;
  
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount = 10, int particleCount = 50);
  /// Emit a specific particle
  import protected bool EmitParticleIndex(int i);
  /// Emit a single particle
  import protected bool EmitSingleParticle();
  /// Emit particles
  import void Emit();
  /// Update all particles
  import void Update();
};

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]

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

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

#define MAX_LERP 1024
#define INT_FRAC 1024

// lerp from percent 0 to 1024
int _Lerp(int start, int end, int percent) {
  if (percent < 0) percent = 0;
  if (percent > MAX_LERP) percent = MAX_LERP;
  return start + ((end - start) * percent) / MAX_LERP;
}

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
function 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/INT_FRAC, p.y/INT_FRAC, p.width, p.height);
#else
  ovr.X = p.x/INT_FRAC;
  ovr.Y = p.y/INT_FRAC;
  ovr.Width = p.width;
  ovr.Height = p.height;
#endif
}

void Particle::Init(ParticleDefinition* def, int x, int y,  int overlayIndex) {
  if(this.overlayIndex >= 0 && this.overlayIndex < MAX_OVERLAYS) {
    InvalidateOverlay(this.overlayIndex);
  }

  this.x = INT_FRAC*(x + def.offsetX); // Offset based on emitter
  this.y = INT_FRAC*(y + def.offsetY); // Offset based on emitter
  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;
  
  this.overlayIndex = overlayIndex;
  this.initialLife = def.life; // Store initial life for transitions
  this.bounces = def.groundHitBounces;
  this.groundY = def.groundY*INT_FRAC;
  if(this.groundY <= 0) {
    this.groundY = 16777216; // a big number so it is not reached
  }

  if (overlayIndex >= 0 && overlayPool[overlayIndex] != null) {
    UpdateOverlayFromParticle(overlayPool[overlayIndex], this);
  }
}

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

// Update the particle state and sync with overlay
void Particle::Update() {
  // alive check is done before calling this function
  this.x += this.vx;
  this.y += this.vy;
  this.vy += this.gravity; // Apply gravity
  int px = this.x/INT_FRAC;
  int py = this.y/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;
  }
  
  int oidx = this.overlayIndex;
  if (oidx >= 0 && overlayPool[oidx] != null) {
    // UpdateOverlayFromParticle(overlayPool[this.overlayIndex], this);
    Overlay* ovr = overlayPool[oidx];
    ovr.Transparency = this.transparency;
    ovr.Graphic = this.sprite;
#ifdef SCRIPT_API_v362
    ovr.SetPosition(px, py, this.width, this.height);
#else
    ovr.X = px;
    ovr.Y = py;
    ovr.Width = this.width;
    ovr.Height = this.height;
#endif
  }
  
  this.life--;
  
  if (this.y >= this.groundY) {
    if (this.bounces) {
      this.vy = -(this.vy * 700)/INT_FRAC; // Invert velocity, reduce it to simulate energy loss
    } else {
      this.life = 0; // Mark particle as dead (cheaper than other things...)
    }
  }
}

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

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

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

protected bool Emitter::EmitParticleIndex(int i)
{
  if(this.particles[i].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;
    }
    this.particles[i].Init(def, this.x, this.y, overlayIndex);
  }
  return true;
}

// Emit a single particle from the emitter
protected bool Emitter::EmitSingleParticle() {
  //System.Log(eLogInfo, ">>begin sp");
  int i = (this.lastEmittedParticle + 1) % this.particleCount;
  
  if(this.EmitParticleIndex(i))
    return false;
  
  //System.Log(eLogInfo, "loop1 sp");
  int loop_at = i;
  for (; i < this.particleCount; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  //System.Log(eLogInfo, "loop2 sp");
  for (i=0; i < loop_at; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  // 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.emitParticleCount; i++) {
    if(this.EmitSingleParticle())
      return;
  }
}

// Update all particles
void Emitter::Update() {
  for (int i = 0; i < this.particleCount; 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);
    }
  }
}

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();
    }
  } 
}
  
ContinuousEmitter GPE;
export GPE;

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

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

>>particle test web demo v4<<

The explosion particle now looks like this

Code: ags
ParticleDefinition* GetExplosionParticle()
{
  ParticleDefinition* explosionParticle = new ParticleDefinition;  
  explosionParticle.sprite = 1+ Random(60);
  explosionParticle.initialSprite = 1;
  explosionParticle.finalSprite = 61;  
  explosionParticle.life = 40;
  explosionParticle.vx = Random(3000) - 1500;
  explosionParticle.vy = Random(3000) - 1500;
  explosionParticle.gravity =  -90;
  explosionParticle.initialTransparency = 15;
  explosionParticle.finalTransparency = 100;
  explosionParticle.initialWidth = 12;
  explosionParticle.finalWidth = 50;
  explosionParticle.initialHeight = 12;
  explosionParticle.finalHeight = 50;
  return explosionParticle;
}

The sprite property of the particle definition is treated like if it was the actually starting sprite, and the initial and final are used to set the range of sprites, this is to be able to add some noise for sprite specifically to let things roll more nicely.

If a particle definition didn't set the initial and final sprite, it will instead use only the sprite set in the sprite property.
#271
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 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;
};

managed struct Particle {
  int sprite;
  int x;
  int 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 initialTransparency;
  int deltaTransparency;
  int initialWidth;
  int deltaWidth;
  int initialHeight;
  int deltaHeight;
  bool bounces;
  int groundY;

  // Initialize the particle with its position, life, velocity, and transparency
  import void Init(ParticleDefinition* def, int x, int y, int overlayIndex);
  import void Update(); // Update particle position and overlay
  import bool IsAlive(); // Check if particle is still alive
};

struct Emitter {
  protected int x;
  protected int y;
  protected int emitParticleCount;
  protected int particleCount;
  protected Particle* particles[]; // Pool of particles
  protected ParticleDefinition* definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  protected int lastEmittedParticle;
  
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount = 10, int particleCount = 50);
  /// Emit a specific particle
  import protected bool EmitParticleIndex(int i);
  /// Emit a single particle
  import protected bool EmitSingleParticle();
  /// Emit particles
  import void Emit();
  /// Update all particles
  import void Update();
};

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]

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

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

#define MAX_LERP 1024
#define INT_FRAC 1024

// lerp from percent 0 to 1024
int _Lerp(int start, int end, int percent) {
  if (percent < 0) percent = 0;
  if (percent > MAX_LERP) percent = MAX_LERP;
  return start + ((end - start) * percent) / MAX_LERP;
}

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
function 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/INT_FRAC, p.y/INT_FRAC, p.width, p.height);
#else
  ovr.X = p.x/INT_FRAC;
  ovr.Y = p.y/INT_FRAC;
  ovr.Width = p.width;
  ovr.Height = p.height;
#endif
}

void Particle::Init(ParticleDefinition* def, int x, int y,  int overlayIndex) {
  if(this.overlayIndex >= 0 && this.overlayIndex < MAX_OVERLAYS) {
    InvalidateOverlay(this.overlayIndex);
  }

  this.x = INT_FRAC*(x + def.offsetX); // Offset based on emitter
  this.y = INT_FRAC*(y + def.offsetY); // Offset based on emitter
  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.overlayIndex = overlayIndex;
  this.initialLife = def.life; // Store initial life for transitions
  this.bounces = def.groundHitBounces;
  this.groundY = def.groundY*INT_FRAC;
  if(this.groundY <= 0) {
    this.groundY = 16777216; // a big number so it is not reached
  }

  if (overlayIndex >= 0 && overlayPool[overlayIndex] != null) {
    UpdateOverlayFromParticle(overlayPool[overlayIndex], this);
  }
}

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

// Update the particle state and sync with overlay
void Particle::Update() {
  // alive check is done before calling this function
  this.x += this.vx;
  this.y += this.vy;
  this.vy += this.gravity; // Apply gravity
  int px = this.x/INT_FRAC;
  int py = this.y/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;
  
  int oidx = this.overlayIndex;
  if (oidx >= 0 && overlayPool[oidx] != null) {
    // UpdateOverlayFromParticle(overlayPool[this.overlayIndex], this);
    Overlay* ovr = overlayPool[oidx];
    ovr.Transparency = this.transparency;
    ovr.Graphic = this.sprite;
#ifdef SCRIPT_API_v362
    ovr.SetPosition(px, py, this.width, this.height);
#else
    ovr.X = px;
    ovr.Y = py;
    ovr.Width = this.width;
    ovr.Height = this.height;
#endif
  }
  
  this.life--;
  
  if (this.y >= this.groundY) {
    if (this.bounces) {
      this.vy = -(this.vy * 700)/INT_FRAC; // Invert velocity, reduce it to simulate energy loss
    } else {
      this.life = 0; // Mark particle as dead (cheaper than other things...)
    }
  }
}

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

void Emitter::SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount)
{
  this.definitions = definitions;
  this.definitionsCount = definitionsCount;
}

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

protected bool Emitter::EmitParticleIndex(int i)
{
  if(this.particles[i].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;
    }
    this.particles[i].Init(def, this.x, this.y, overlayIndex);
  }
  return true;
}

// Emit a single particle from the emitter
protected bool Emitter::EmitSingleParticle() {
  //System.Log(eLogInfo, ">>begin sp");
  int i = (this.lastEmittedParticle + 1) % this.particleCount;
  
  if(this.EmitParticleIndex(i))
    return false;
  
  //System.Log(eLogInfo, "loop1 sp");
  int loop_at = i;
  for (; i < this.particleCount; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  //System.Log(eLogInfo, "loop2 sp");
  for (i=0; i < loop_at; i++) {
    if (!this.particles[i].IsAlive())
    {
      this.EmitParticleIndex(i);
      return false;
    }
  }
  
  // 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.emitParticleCount; i++) {
    if(this.EmitSingleParticle())
      return;
  }
}

// Update all particles
void Emitter::Update() {
  for (int i = 0; i < this.particleCount; 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);
    }
  }
}

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();
    }
  } 
}
  
ContinuousEmitter GPE;
export GPE;

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

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

I haven't actually changed room1 here, but I have played with changing the module code to try to improve a bit of performance... In the above version I upped the amount of particles to 2048.


I had one idea for additional performance which was to run the "particle simulation" for what would be 5 frames and interpolate the in-between, but I didn't figure a way to do this that actually improved the perceived performance - plus ended up adding a few bugs, so I scrapped. The performance improvements I did here ended up being the most obvious in just focusing in reducing the amount of code ran. Also internally I no longer use floats anywhere - to avoid conversion between int and floats.

In the next few days I will probably do a few polishes to this and try to actually add some more visually interesting stuff here and hopefully actually put a module for release.

Spoiler
I wanted to be able to think if there was some API change to be done in Overlays in the engine that could unlock some faster way to do something, just to improve performance for particles beyond the current one, but short of throwing a few int arrays as input parameters I think I am out of ideas.
[close]
#272
This example I made has a cap of 512 Overlays by 512 Particles, but I believe it should be able to run up to 2048 without many issues - I made a sprite scaler 3D like engine using overlays and managed to get to 4096 on screen things going before slow down.

Regarding the resolution, you pay for it twice, once to read from the sprite file to the sprite cache and another time to read the sprite from the sprite cache to the texture cache. So once these are cached they are reused, and the resolution doesn't matter much, as long they all fit in memory.

For now I am mostly about playing with the physics of the particle, I think I will look into the visuals of the individual particles and only latter look into the emitter (for things like movement of it), but for now I am "outsourcing" their movement at the discretion of the user  (laugh)
#273
Tint for something like fire to have the yellow to reddish transition is not hard to add, the only issue is Overlay tint is ags4 only. I think maybe it's not that much to backport to ags3 but from memory I don't remember much.

Rotation of particles is ags4 too - and It's a lot of stuff so it will be for sure ags4 only.

One hack that I can add to support these fakely is to be able to set the sprite of each particle and also being able to set a "final sprite", so it would increment the sprite number until the final one, which would enable to make the sprite change along with it's life. This would require a bit of care of the person using the module but it could enable some fun effects.

I think these aren't hard to add, so I will add these things to a list.

I also want to add some sort of collision check so you could use the particles for something like coins that drops from a monster you kill and then you would be able to collect - perhaps would need to have some gravity well support too if you would just be able to attract these.

Overall the balance is mostly in trying things and see how fast things can run. I will probably remove the floating points I am using internally and rewrite using only int to improve performance slightly.

Edit: ah the video wasn't working before, I watched it now, that is pretty cool, is this in ags4 with blending modes? I can't figure other approach.

One thing that one can do with these that I had the idea just now is to record mouse positions and throw them in a script array for playback if you want to make some "forest spirit" that points to things and want to keep the movement organical but don't want to have to note down all positions manually to enable this.
#274
Other thing is repetition, like, if say you are going to have like 200 animated objects in the room but all of them are running the exact same animation, AGS will just repeat the texture graphics for all of them, which is fairly fast, if the 200 objects are different animations, then things will perform worse. Overall it really depends on what exactly you are doing.
#275
I did an improvement in the Sparkle particle and added some small physics, so you can set some ground where the particle may hit and either "die" or bounce. I am unsure yet if the particle should get life go to 0 if it's bounce is not set or just stay there and let it's life end. I may instead replace ground hit with an enum and then enable one of the three for selection. But I also prefer to simplify if possible to keep the update loop tight.

I kinda want to experiment with ideas and later simplify things.

Edit:ok, decided to play a bit more and made an improvement and added a new ContinuousEmitter! It can be run with ContinuousUpdate instead of calling Update on rep exec. I made the GlobalParticleEmitter be a ContinuousEmitter instead.



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

managed struct ParticleDefinition {
    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 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;
    int groundX;
    int groundWidth;
};

managed struct Particle {
    float x;
    float y;
    int life;
    int initialLife;
    int overlayIndex; // This refers to the overlay in the overlay pool
    float vx; // x velocity
    float vy; // y velocity
    float gravity; // this is vertical acceleration downwards
    int transparency;
    int width;
    int height;
    int initialTransparency; 
    int finalTransparency; 
    int initialWidth; 
    int finalWidth; 
    int initialHeight; 
    int finalHeight;
    bool bounces;
    int groundY;
    int groundX;
    int groundWidth;

    // Initialize the particle with its position, life, velocity, and transparency
    import void Init(ParticleDefinition* def, int x, int y, int overlayIndex);
    import void Update(); // Update particle position and overlay
    import bool IsAlive(); // Check if particle is still alive
};

struct Emitter {
  protected int x;
  protected int y;
  protected int particleLife;
  protected int emitParticleCount;
  protected int particleCount;
  protected int sprite; // The sprite slot to use for particles
  protected int gravity;
  protected Particle* particles[]; // Pool of particles
  protected ParticleDefinition* definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  
  /// Set sprite
  import void SetSprite(int graphic);  
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount = 10, int particleCount = 50, int sprite = 0, int gravity = 0);
  /// Emit a single particle
  import void EmitSingleParticle();  
  /// Emit particles
  import void Emit(); 
  /// Update all particles
  import void Update();
};

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]

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

#define MAX_OVERLAYS 512
Overlay* overlayPool[MAX_OVERLAYS];
bool overlayUsed[MAX_OVERLAYS];
int lastUsed;

#define MAX_LERP 1024

// lerp from percent 0 to 1024
int _Lerp(int start, int end, int percent) {
  if (percent < 0) percent = 0;
  if (percent > MAX_LERP) percent = MAX_LERP;

  // Calculate the interpolated value
  return start + ((end - start) * percent) / MAX_LERP;
}

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

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

void UpdateOverlayFromParticle(Overlay* ovr, Particle* p)
{
  ovr.X = FloatToInt(p.x);
  ovr.Y = FloatToInt(p.y);
  ovr.Transparency = p.transparency;
  ovr.Width = p.width;
  ovr.Height = p.height;
}

void Particle::Init(ParticleDefinition* def, int x, int y,  int overlayIndex) {
  if(this.overlayIndex >= 0 && this.overlayIndex < MAX_OVERLAYS) {
    InvalidateOverlay(this.overlayIndex);
  }

  this.x = IntToFloat(x + def.offsetX); // Offset based on emitter
  this.y = IntToFloat(y + def.offsetY); // Offset based on emitter
  this.life = def.life;
  this.vx = IntToFloat(def.vx)/1000.0;
  this.vy = IntToFloat(def.vy)/1000.0;
  this.gravity = IntToFloat(def.gravity)/1000.0;
  this.transparency = def.initialTransparency;
  this.initialTransparency = def.initialTransparency;
  this.finalTransparency = def.finalTransparency;
  this.width = def.initialWidth;
  this.initialWidth = def.initialWidth;
  this.finalWidth = def.finalWidth;
  this.height = def.initialHeight;
  this.initialHeight = def.initialHeight;
  this.finalHeight = def.finalHeight;
  this.overlayIndex = overlayIndex;
  this.initialLife = def.life; // Store initial life for transitions
  this.bounces = def.groundHitBounces;
  this.groundY = def.groundY;
  this.groundX = def.groundX;
  this.groundWidth = def.groundWidth;
  if(this.groundY > 0) {
    if(this.groundWidth <= 0 ) {
      this.groundWidth = 8192;
      this.groundX = -1024;
    }
  } else {
    this.groundY = 0;
    this.groundX = 0;
    this.groundWidth = 0;
  }

  if (overlayIndex >= 0 && overlayPool[overlayIndex] != null) {
    UpdateOverlayFromParticle(overlayPool[overlayIndex], this);
  }
}

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

// Update the particle state and sync with overlay
void Particle::Update() {
  if (this.IsAlive()) {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += this.gravity; // Apply gravity
    this.life--;
    int px = FloatToInt(this.x);
    int py = FloatToInt(this.y);

    // Calculate the scaling and transparency transitions based on life
    int lifeRatio =  MAX_LERP - ((this.life * MAX_LERP) / this.initialLife); // 0 to 1024
    this.transparency = _Lerp(this.initialTransparency, this.finalTransparency, lifeRatio);
    this.width = _Lerp(this.initialWidth, this.finalWidth, lifeRatio);
    this.height = _Lerp(this.initialHeight, this.finalHeight, lifeRatio);

    if (this.overlayIndex >= 0 && overlayPool[this.overlayIndex] != null) {
      UpdateOverlayFromParticle(overlayPool[this.overlayIndex], this);
    }    
    
    if ((py >= this.groundY) && (px >= this.groundX) && (px <= (this.groundX + this.groundWidth))) {
      if (this.bounces) {
        this.vy = -this.vy * 0.7; // Invert velocity, reduce it to simulate energy loss
      } else {
        this.life = 0; // Mark particle as dead
      }
    }
  } else {
    // Remove overlay if life is over
    if (this.overlayIndex >= 0) {
      this.overlayIndex = InvalidateOverlay(this.overlayIndex); // Invalidate the overlay index
    }
  }
}

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

void Emitter::SetSprite(int graphic)
{
  this.sprite = graphic;
}

void Emitter::SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount)
{
  this.definitions = definitions;
  this.definitionsCount = definitionsCount;
}

// Initialize the emitter with position, particle definitions, and specific parameters
void Emitter::Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount, int particleCount, int sprite, int gravity) {
  this.SetPosition(x, y);
  this.particleCount = particleCount;
  this.SetSprite(sprite);
  this.gravity = gravity;
  this.SetParticleDefinitions(definitions, definitionsCount);
  this.emitParticleCount = emitParticleCount;
  this.particles = new Particle[particleCount];
  for (int i = 0; i < particleCount; i++) {
    this.particles[i] = new Particle;
  }
}

// Emit a single particle from the emitter
void Emitter::EmitSingleParticle() {
  for (int i = 0; i < this.particleCount; i++) {
    if (this.particles[i].IsAlive())
    {
      continue;
    }
    
    // Reuse dead particle if it's not alive anymore
    int overlayIndex = GetAvailableOverlayIndex();
    if (overlayIndex >= 0) {
      Overlay* ovr = Overlay.CreateGraphical(this.x, this.y, this.sprite);
      overlayPool[overlayIndex] = ovr;

      // 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];

      this.particles[i].Init(def, this.x, this.y, overlayIndex);
    }
    return;
  }
}

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

// Update all particles
void Emitter::Update() {
  for (int i = 0; i < this.particleCount; i++) {
    if (this.particles[i].IsAlive()) {
      this.particles[i].Update();
    } else if (this.particles[i].overlayIndex >= 0) {
      // Ensure overlays are freed for dead particles
      this.particles[i].overlayIndex = InvalidateOverlay(this.particles[i].overlayIndex); // Invalidate the overlay index
    }
  }
}

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();
    }
  } 
}
  
ContinuousEmitter GPE;
export GPE;

void game_start()
{
  ParticleDefinition* d[];
  GPE.Init(Screen.Width/2, Screen.Height/2, d, 0, MAX_OVERLAYS/14, MAX_OVERLAYS, 0, 0);
}

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

room1.asc
Spoiler
Code: ags
// room script file

//Emitter emt;

function hGlowingOrb_Look(Hotspot *thisHotspot, CursorMode mode)
{
  player.Say("It is the second best glowing orb that I've seen today.");
}

ParticleDefinition* GetFireworksParticle()
{
  ParticleDefinition* fireworksParticle = new ParticleDefinition;
  fireworksParticle.life = 40;
  fireworksParticle.vx = Random(4000) - 2000; // Random outward velocity
  fireworksParticle.vy = Random(4000) - 2000;
  fireworksParticle.gravity = 0; // No gravity
  fireworksParticle.initialTransparency = 0;
  fireworksParticle.finalTransparency = 100;
  fireworksParticle.initialWidth = 2;
  fireworksParticle.finalWidth = 20; // Expanding outward
  fireworksParticle.initialHeight = 2;
  fireworksParticle.finalHeight = 20;
  return fireworksParticle;
}

ParticleDefinition* GetSparkleParticle()
{
  ParticleDefinition* sparkleParticle = new ParticleDefinition;
  sparkleParticle.life = 50;
  sparkleParticle.vx = Random(3000) - 1000;
  sparkleParticle.vy = Random(3000) - 1000;
  sparkleParticle.initialTransparency = 0;
  sparkleParticle.finalTransparency = 100;
  sparkleParticle.initialWidth = 3;
  sparkleParticle.finalWidth = 8;
  sparkleParticle.initialHeight = 3;
  sparkleParticle.finalHeight = 8;
  sparkleParticle.gravity = 100;
  sparkleParticle.groundY = 154;
  sparkleParticle.groundHitBounces = true;
  return sparkleParticle;
}

ParticleDefinition* GetExplosionParticle()
{
  ParticleDefinition* explosionParticle = new ParticleDefinition;
  explosionParticle.life = 30;
  explosionParticle.vx = Random(6000) - 3000;
  explosionParticle.vy = Random(6000) - 3000;
  explosionParticle.gravity =  -1000;
  explosionParticle.initialTransparency = 15;
  explosionParticle.finalTransparency = 100;
  explosionParticle.initialWidth = 15;
  explosionParticle.finalWidth = 30;
  explosionParticle.initialHeight = 15;
  explosionParticle.finalHeight = 30;
  return explosionParticle;
}

ParticleDefinition* GetSmokeParticle()
{
  ParticleDefinition* smokeParticle = new ParticleDefinition;
  smokeParticle.life = 40+Random(14);
  smokeParticle.vy = -1000-Random(1000);
  smokeParticle.initialTransparency = 0;
  smokeParticle.finalTransparency = 100;
  smokeParticle.initialWidth = 10+Random(2);
  smokeParticle.finalWidth = 20+Random(2);
  smokeParticle.initialHeight = 20+Random(2);
  smokeParticle.finalHeight = 10+Random(2);
  return smokeParticle;
}

ParticleDefinition* GetBubbleParticle()
{
  ParticleDefinition* bubbleParticle = new ParticleDefinition;
  bubbleParticle.life = 60;
  bubbleParticle.vx = Random(500) - 250; // Small horizontal drift
  bubbleParticle.vy = -1000 - Random(500); // Rising upwards
  bubbleParticle.gravity = -200; // Rising effect
  bubbleParticle.initialTransparency = 30;
  bubbleParticle.finalTransparency = 100;
  bubbleParticle.initialWidth = 5;
  bubbleParticle.finalWidth = 15; // Expands as it rises
  bubbleParticle.initialHeight = 5;
  bubbleParticle.finalHeight = 15;
  return bubbleParticle;
}

ParticleDefinition* GetRainParticle()
{
  ParticleDefinition* rainParticle = new ParticleDefinition;
  rainParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  rainParticle.offsetY = -Random(30);
  rainParticle.life = 50;
  rainParticle.vx = Random(500) - 250; // Slight horizontal movement
  rainParticle.vy = 3000; // Falling down quickly
  rainParticle.gravity = 180; // Light gravity effect
  rainParticle.initialTransparency = 30;
  rainParticle.finalTransparency = 80;
  rainParticle.initialWidth = 2;
  rainParticle.finalWidth = 2;
  rainParticle.initialHeight = 10;
  rainParticle.finalHeight = 15; // Lengthening as it falls
  return rainParticle;
}

ParticleDefinition* GetFireParticle()
{
  ParticleDefinition* fireParticle = new ParticleDefinition;
  fireParticle.life = 35;
  fireParticle.vx = Random(1000) - 500; // Small horizontal variance
  fireParticle.vy = -1200 - Random(500); // Rising upward
  fireParticle.gravity = -50; // Slow upward pull
  fireParticle.initialTransparency = 50;
  fireParticle.finalTransparency = 100; // Disappears as it rises
  fireParticle.initialWidth = 10;
  fireParticle.finalWidth = 20; // Expands as it rises
  fireParticle.initialHeight = 10;
  fireParticle.finalHeight = 15;
  return fireParticle;
}

ParticleDefinition* GetSnowParticle()
{
  ParticleDefinition* snowParticle = new ParticleDefinition;
  snowParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  snowParticle.offsetY = -Random(30);
  snowParticle.life = 160;
  snowParticle.vx = Random(300) - 150; // Slight horizontal drift
  snowParticle.vy = Random(300) + 220; // Slow downward movement
  snowParticle.gravity = 10; // Minimal gravity effect
  snowParticle.initialTransparency = 50;
  snowParticle.finalTransparency = 75;
  snowParticle.initialWidth = 4;
  snowParticle.finalWidth = 6; // Slight expansion as it falls
  snowParticle.initialHeight = 4;
  snowParticle.finalHeight = 6;
  return snowParticle;
}

enum PresetParticleType {
  ePPT_Fireworks, 
  ePPT_Sparkle, 
  ePPT_Explosion, 
  ePPT_Smoke, 
  ePPT_Bubble, 
  ePPT_Rain, 
  ePPT_Fire, 
  ePPT_Snow
};

#define ePPT_Last ePPT_Snow

ParticleDefinition* [] GetParticleDefinitionsArrayByType(PresetParticleType type, int count)
{
  ParticleDefinition* definitions[] = new ParticleDefinition[count];
  int i;
  switch(type) {
    case ePPT_Fireworks:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireworksParticle();
      }
    break;
    case ePPT_Sparkle:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSparkleParticle();
      }
    break;
    case ePPT_Explosion:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetExplosionParticle();
      }
    break;
    case ePPT_Smoke:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSmokeParticle();
      }
    break;
    case ePPT_Bubble:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetBubbleParticle();
      }
    break;
    case ePPT_Rain:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetRainParticle();
      }
    break;
    case ePPT_Fire:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireParticle();
      }
    break;
    case ePPT_Snow:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSnowParticle();
      }
    break;
  }  
  return definitions;
}

String GetTypeName(PresetParticleType type) {  
  switch(type) {
    case ePPT_Fireworks:
      return "Fireworks";
    case ePPT_Sparkle:
      return "Sparkle";
    case ePPT_Explosion:
      return "Explosion";
    case ePPT_Smoke:
      return "Smoke";
    case ePPT_Bubble:
      return "Bubble";
    case ePPT_Rain:
      return "Rain";
    case ePPT_Fire:
      return "Fire";
    case ePPT_Snow:
      return "Snow";
    default:
      return "Unknown";
  }  
}

void SetEmitterToType(PresetParticleType type)
{
  int definitions_count = 2048;
  ParticleDefinition* definitions[] = GetParticleDefinitionsArrayByType(type, definitions_count);
  GPE.SetParticleDefinitions(definitions, definitions_count);
  lbl_particle_selected.Text = GetTypeName(type);
}

int particle_type;
void on_call (int value)
{
  if(value == 1) {
    particle_type++;
    if(particle_type> ePPT_Last) {
      particle_type = 1;
    }
  }
  SetEmitterToType(particle_type);
  if(particle_type == ePPT_Rain || particle_type == ePPT_Snow) {
    GPE.SetPosition(Screen.Width/2, 0);
    GPE.StartEmitting();
  } else {
    GPE.StopEmitting();
  }
}

function room_Load()
{  
  SetEmitterToType(ePPT_Fireworks);
}

void on_mouse_click(MouseButton button)
{
  if(particle_type == ePPT_Rain || particle_type == ePPT_Snow)
    return;
  GPE.SetPosition(mouse.x, mouse.y);
  GPE.Emit();
}

int mb_press;
function room_RepExec()
{
  if(particle_type == ePPT_Rain || particle_type == ePPT_Snow)
    return;
    
  if(mouse.IsButtonDown(eMouseLeft))
    mb_press++;
  else
    mb_press = 0;
  
  if(mb_press > 10) {
    GPE.SetPosition(mouse.x, mouse.y);
    GPE.StartEmitting(5);
  } else {
    GPE.StopEmitting();
  }
}
[close]
#276
Since AGS has no Keyboard movement by default and you can implement your own, just wanted to give plug to my own module because I like it better than whatever comes with the templates, I present you my controlz script module (GitHub repo here). (note if you use it you will have to also disable whatever you are using now because otherwise it won't work well)

If you are using something else though, it would be nice if you could share your code for keyboard movement.
#277
What CW said are the functions that are at disposal.

Just was thinking about my previously mentioned module... If you want to try hacking around at it.

Looking at my fancy module, I think the module has an idea of position of tokes in it's draw method here. It looks like it's possible to create an additional function that can test given an x, y position, in which token it is.

But API wise I have zero idea how to have this work in this module, I guess it would be something in either FancyTextBase or FancyTextBox and have some tag to mark something in the text, but I unsure of which would be the best way to handle there.
#278
I decided to play around with the idea of a particle system just for fun, first, here is the demo


Here is the sketch of the module header and script

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

managed struct ParticleDefinition {
    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 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;
    int groundX;
    int groundWidth;
};

managed struct Particle {
    float x;
    float y;
    int life;
    int initialLife;
    int overlayIndex; // This refers to the overlay in the overlay pool
    float vx; // x velocity
    float vy; // y velocity
    float gravity; // this is vertical acceleration downwards
    int transparency;
    int width;
    int height;
    int initialTransparency; 
    int finalTransparency; 
    int initialWidth; 
    int finalWidth; 
    int initialHeight; 
    int finalHeight;
    bool bounces;
    int groundY;
    int groundX;
    int groundWidth;

    // Initialize the particle with its position, life, velocity, and transparency
    import void Init(ParticleDefinition* def, int x, int y, int overlayIndex);
    import void Update(); // Update particle position and overlay
    import bool IsAlive(); // Check if particle is still alive
};

struct Emitter {
  protected int x;
  protected int y;
  protected int particleLife;
  protected int emitParticleCount;
  protected int particleCount;
  protected int sprite; // The sprite slot to use for particles
  protected int gravity;
  protected Particle* particles[]; // Pool of particles
  protected ParticleDefinition* definitions[]; // Array of particle definitions
  protected int definitionsCount; // Count of particle definitions
  
  /// Set sprite
  import void SetSprite(int graphic);  
  /// Set emitter possible particle definitions
  import void SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount);
  /// Update emitter position
  import void SetPosition(int x, int y);

  /// Initialize the emitter
  import void Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount = 10, int particleCount = 50, int sprite = 0, int gravity = 0);
  /// Emit a single particle
  import void EmitSingleParticle();  
  /// Emit particles
  import void Emit(); 
  /// Update all particles
  import void Update();
};

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

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

#define MAX_OVERLAYS 512
Overlay* overlayPool[MAX_OVERLAYS];
bool overlayUsed[MAX_OVERLAYS];
int lastUsed;
Emitter GPE;
export GPE;

#define MAX_LERP 1024

// lerp from percent 0 to 1024
int _Lerp(int start, int end, int percent) {
  if (percent < 0) percent = 0;
  if (percent > MAX_LERP) percent = MAX_LERP;

  // Calculate the interpolated value
  return start + ((end - start) * percent) / MAX_LERP;
}

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

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

void UpdateOverlayFromParticle(Overlay* ovr, Particle* p)
{
  ovr.X = FloatToInt(p.x);
  ovr.Y = FloatToInt(p.y);
  ovr.Transparency = p.transparency;
  ovr.Width = p.width;
  ovr.Height = p.height;
}

void Particle::Init(ParticleDefinition* def, int x, int y,  int overlayIndex) {
  if(this.overlayIndex >= 0 && this.overlayIndex < MAX_OVERLAYS) {
    InvalidateOverlay(this.overlayIndex);
  }

  this.x = IntToFloat(x + def.offsetX); // Offset based on emitter
  this.y = IntToFloat(y + def.offsetY); // Offset based on emitter
  this.life = def.life;
  this.vx = IntToFloat(def.vx)/1000.0;
  this.vy = IntToFloat(def.vy)/1000.0;
  this.gravity = IntToFloat(def.gravity)/1000.0;
  this.transparency = def.initialTransparency;
  this.initialTransparency = def.initialTransparency;
  this.finalTransparency = def.finalTransparency;
  this.width = def.initialWidth;
  this.initialWidth = def.initialWidth;
  this.finalWidth = def.finalWidth;
  this.height = def.initialHeight;
  this.initialHeight = def.initialHeight;
  this.finalHeight = def.finalHeight;
  this.overlayIndex = overlayIndex;
  this.initialLife = def.life; // Store initial life for transitions
  this.bounces = def.groundHitBounces;
  this.groundY = def.groundY;
  this.groundX = def.groundX;
  this.groundWidth = def.groundWidth;
  if(this.groundY > 0) {
    if(this.groundWidth <= 0 ) {
      this.groundWidth = 8192;
      this.groundX = -1024;
    }
  } else {
    this.groundY = 0;
    this.groundX = 0;
    this.groundWidth = 0;
  }

  if (overlayIndex >= 0 && overlayPool[overlayIndex] != null) {
    UpdateOverlayFromParticle(overlayPool[overlayIndex], this);
  }
}

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

// Update the particle state and sync with overlay
void Particle::Update() {
  if (this.IsAlive()) {
    this.x += this.vx;
    this.y += this.vy;
    this.vy += this.gravity; // Apply gravity
    this.life--;
    int px = FloatToInt(this.x);
    int py = FloatToInt(this.y);

    // Calculate the scaling and transparency transitions based on life
    int lifeRatio =  MAX_LERP - ((this.life * MAX_LERP) / this.initialLife); // 0 to 1024
    this.transparency = _Lerp(this.initialTransparency, this.finalTransparency, lifeRatio);
    this.width = _Lerp(this.initialWidth, this.finalWidth, lifeRatio);
    this.height = _Lerp(this.initialHeight, this.finalHeight, lifeRatio);

    if (this.overlayIndex >= 0 && overlayPool[this.overlayIndex] != null) {
      UpdateOverlayFromParticle(overlayPool[this.overlayIndex], this);
    }    
    
    if ((py >= this.groundY) && (px >= this.groundX) && (px <= (this.groundX + this.groundWidth))) {
      if (this.bounces) {
        this.vy = -this.vy * 0.7; // Invert velocity, reduce it to simulate energy loss
      } else {
        this.life = 0; // Mark particle as dead
      }
    }
  } else {
    // Remove overlay if life is over
    if (this.overlayIndex >= 0) {
      this.overlayIndex = InvalidateOverlay(this.overlayIndex); // Invalidate the overlay index
    }
  }
}

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

void Emitter::SetSprite(int graphic)
{
  this.sprite = graphic;
}

void Emitter::SetParticleDefinitions(ParticleDefinition* definitions[], int definitionsCount)
{
  this.definitions = definitions;
  this.definitionsCount = definitionsCount;
}

// Initialize the emitter with position, particle definitions, and specific parameters
void Emitter::Init(int x, int y, ParticleDefinition* definitions[], int definitionsCount, int emitParticleCount, int particleCount, int sprite, int gravity) {
  this.SetPosition(x, y);
  this.particleCount = particleCount;
  this.SetSprite(sprite);
  this.gravity = gravity;
  this.SetParticleDefinitions(definitions, definitionsCount);
  this.emitParticleCount = emitParticleCount;
  this.particles = new Particle[particleCount];
  for (int i = 0; i < particleCount; i++) {
    this.particles[i] = new Particle;
  }
}

// Emit a single particle from the emitter
void Emitter::EmitSingleParticle() {
  for (int i = 0; i < this.particleCount; i++) {
    if (this.particles[i].IsAlive())
    {
      continue;
    }
    
    // Reuse dead particle if it's not alive anymore
    int overlayIndex = GetAvailableOverlayIndex();
    if (overlayIndex >= 0) {
      Overlay* ovr = Overlay.CreateGraphical(this.x, this.y, this.sprite);
      overlayPool[overlayIndex] = ovr;

      // 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];

      this.particles[i].Init(def, this.x, this.y, overlayIndex);
    }
    return;
  }
}

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

// Update all particles
void Emitter::Update() {
  for (int i = 0; i < this.particleCount; i++) {
    if (this.particles[i].IsAlive()) {
      this.particles[i].Update();
    } else if (this.particles[i].overlayIndex >= 0) {
      // Ensure overlays are freed for dead particles
      this.particles[i].overlayIndex = InvalidateOverlay(this.particles[i].overlayIndex); // Invalidate the overlay index
    }
  }
}

void game_start()
{
  ParticleDefinition* d[];
  GPE.Init(Screen.Width/2, Screen.Height/2, d, 0, MAX_OVERLAYS/14, MAX_OVERLAYS/2, 0, 0);
}

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

And here is the room script for the demo above
room1.asc
Spoiler
Code: ags
// room script file

//Emitter emt;

function hGlowingOrb_Look(Hotspot *thisHotspot, CursorMode mode)
{
  player.Say("It is the second best glowing orb that I've seen today.");
}

ParticleDefinition* GetFireworksParticle()
{
  ParticleDefinition* fireworksParticle = new ParticleDefinition;
  fireworksParticle.life = 40;
  fireworksParticle.vx = Random(4000) - 2000; // Random outward velocity
  fireworksParticle.vy = Random(4000) - 2000;
  fireworksParticle.gravity = 0; // No gravity
  fireworksParticle.initialTransparency = 0;
  fireworksParticle.finalTransparency = 100;
  fireworksParticle.initialWidth = 2;
  fireworksParticle.finalWidth = 20; // Expanding outward
  fireworksParticle.initialHeight = 2;
  fireworksParticle.finalHeight = 20;
  return fireworksParticle;
}

ParticleDefinition* GetSparkleParticle()
{
  ParticleDefinition* sparkleParticle = new ParticleDefinition;
  sparkleParticle.life = 50;
  sparkleParticle.vx = Random(3000) - 1000;
  sparkleParticle.vy = Random(3000) - 1000;
  sparkleParticle.initialTransparency = 0;
  sparkleParticle.finalTransparency = 100;
  sparkleParticle.initialWidth = 3;
  sparkleParticle.finalWidth = 8;
  sparkleParticle.initialHeight = 3;
  sparkleParticle.finalHeight = 8;
  sparkleParticle.gravity = 100;
  sparkleParticle.groundY = 154;
  sparkleParticle.groundHitBounces = true;
  return sparkleParticle;
}

ParticleDefinition* GetExplosionParticle()
{
  ParticleDefinition* explosionParticle = new ParticleDefinition;
  explosionParticle.life = 30;
  explosionParticle.vx = Random(6000) - 3000;
  explosionParticle.vy = Random(6000) - 3000;
  explosionParticle.gravity =  -1000;
  explosionParticle.initialTransparency = 15;
  explosionParticle.finalTransparency = 100;
  explosionParticle.initialWidth = 15;
  explosionParticle.finalWidth = 30;
  explosionParticle.initialHeight = 15;
  explosionParticle.finalHeight = 30;
  return explosionParticle;
}

ParticleDefinition* GetSmokeParticle()
{
  ParticleDefinition* smokeParticle = new ParticleDefinition;
  smokeParticle.life = 40+Random(14);
  smokeParticle.vy = -1000-Random(1000);
  smokeParticle.initialTransparency = 0;
  smokeParticle.finalTransparency = 100;
  smokeParticle.initialWidth = 10+Random(2);
  smokeParticle.finalWidth = 20+Random(2);
  smokeParticle.initialHeight = 20+Random(2);
  smokeParticle.finalHeight = 10+Random(2);
  return smokeParticle;
}

ParticleDefinition* GetBubbleParticle()
{
  ParticleDefinition* bubbleParticle = new ParticleDefinition;
  bubbleParticle.life = 60;
  bubbleParticle.vx = Random(500) - 250; // Small horizontal drift
  bubbleParticle.vy = -1000 - Random(500); // Rising upwards
  bubbleParticle.gravity = -200; // Rising effect
  bubbleParticle.initialTransparency = 30;
  bubbleParticle.finalTransparency = 100;
  bubbleParticle.initialWidth = 5;
  bubbleParticle.finalWidth = 15; // Expands as it rises
  bubbleParticle.initialHeight = 5;
  bubbleParticle.finalHeight = 15;
  return bubbleParticle;
}

ParticleDefinition* GetRainParticle()
{
  ParticleDefinition* rainParticle = new ParticleDefinition;
  rainParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  rainParticle.offsetY = -Random(30);
  rainParticle.life = 50;
  rainParticle.vx = Random(500) - 250; // Slight horizontal movement
  rainParticle.vy = 3000; // Falling down quickly
  rainParticle.gravity = 200; // Light gravity effect
  rainParticle.initialTransparency = 30;
  rainParticle.finalTransparency = 80;
  rainParticle.initialWidth = 2;
  rainParticle.finalWidth = 2;
  rainParticle.initialHeight = 10;
  rainParticle.finalHeight = 15; // Lengthening as it falls
  return rainParticle;
}

ParticleDefinition* GetFireParticle()
{
  ParticleDefinition* fireParticle = new ParticleDefinition;
  fireParticle.life = 35;
  fireParticle.vx = Random(1000) - 500; // Small horizontal variance
  fireParticle.vy = -1500 - Random(500); // Rising upward
  fireParticle.gravity = -50; // Slow upward pull
  fireParticle.initialTransparency = 50;
  fireParticle.finalTransparency = 100; // Disappears as it rises
  fireParticle.initialWidth = 10;
  fireParticle.finalWidth = 20; // Expands as it rises
  fireParticle.initialHeight = 10;
  fireParticle.finalHeight = 15;
  return fireParticle;
}

ParticleDefinition* GetSnowParticle()
{
  ParticleDefinition* snowParticle = new ParticleDefinition;
  snowParticle.offsetX = Random(Screen.Width) - (Screen.Width/2);
  snowParticle.offsetY = -Random(30);
  snowParticle.life = 150;
  snowParticle.vx = Random(300) - 150; // Slight horizontal drift
  snowParticle.vy = Random(300) + 300; // Slow downward movement
  snowParticle.gravity = 15; // Minimal gravity effect
  snowParticle.initialTransparency = 50;
  snowParticle.finalTransparency = 80;
  snowParticle.initialWidth = 4;
  snowParticle.finalWidth = 6; // Slight expansion as it falls
  snowParticle.initialHeight = 4;
  snowParticle.finalHeight = 6;
  return snowParticle;
}

enum PresetParticleType {
  ePPT_Fireworks, 
  ePPT_Sparkle, 
  ePPT_Explosion, 
  ePPT_Smoke, 
  ePPT_Bubble, 
  ePPT_Rain, 
  ePPT_Fire, 
  ePPT_Snow
};

#define ePPT_Last ePPT_Snow

ParticleDefinition* [] GetParticleDefinitionsArrayByType(PresetParticleType type, int count)
{
  ParticleDefinition* definitions[] = new ParticleDefinition[count];
  int i;
  switch(type) {
    case ePPT_Fireworks:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireworksParticle();
      }
    break;
    case ePPT_Sparkle:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSparkleParticle();
      }
    break;
    case ePPT_Explosion:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetExplosionParticle();
      }
    break;
    case ePPT_Smoke:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSmokeParticle();
      }
    break;
    case ePPT_Bubble:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetBubbleParticle();
      }
    break;
    case ePPT_Rain:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetRainParticle();
      }
    break;
    case ePPT_Fire:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetFireParticle();
      }
    break;
    case ePPT_Snow:
      for(i=0; i<count; i++)
      {
        definitions[i] = GetSnowParticle();
      }
    break;
  }  
  return definitions;
}

String GetTypeName(PresetParticleType type) {  
  switch(type) {
    case ePPT_Fireworks:
      return "Fireworks";
    case ePPT_Sparkle:
      return "Sparkle";
    case ePPT_Explosion:
      return "Explosion";
    case ePPT_Smoke:
      return "Smoke";
    case ePPT_Bubble:
      return "Bubble";
    case ePPT_Rain:
      return "Rain";
    case ePPT_Fire:
      return "Fire";
    case ePPT_Snow:
      return "Snow";
    default:
      return "Unknown";
  }  
}

void SetEmitterToType(PresetParticleType type)
{
  int definitions_count = 2048;
  ParticleDefinition* definitions[] = GetParticleDefinitionsArrayByType(type, definitions_count);
  GPE.SetParticleDefinitions(definitions, definitions_count);
  lbl_particle_selected.Text = GetTypeName(type);
}

int particle_type;
void on_call (int value)
{
  if(value == 1) {
    particle_type++;
    if(particle_type> ePPT_Last) {
      particle_type = 1;
    }
  }
  SetEmitterToType(particle_type);
}

function room_Load()
{  
  SetEmitterToType(ePPT_Fireworks);
}

void on_mouse_click(MouseButton button)
{
  int mx = mouse.x;
  int my = mouse.y;
  GPE.SetPosition(mx, my);
  GPE.Emit();
}
[close]

OK, so I started to play with this thing by sheer poking around, and there is a lot that is unfinished, but I kinda was trying to think on what does Particle management system are expected to have. I used some lerp there that has both initial and final state so one can see this could easily put on top the curves used by the tween module, but what else could be done here?

I want to keep at minimal Overlay manipulation. Other thing is I kind have no idea in a way that would be nice to actually setup things, I did a weird way that you have a particle definition, then you make an array of those and pass to the emitter, and then the emitter select at random from the definitions, the idea here is to avoid to have complex random functions per attribute, but I probably still need to have some modifier aspect to each attribute.

Anyway, just wanted to kick around the idea of a particle module and see ideas on this.

Edit: I had an idea that the emitter could have types like point or rectangle (lacking of other ideas here) and if it's a rectangle it would spawn a particle randomly in a point inside the rectangle. My idea is that perhaps doing that I could make things like rain and other ideas. Edit2: I actually can hack around this using the offsetX and offsetY in the particle itself!

Other idea is besides having the Emit functionality, to also have a RepeatedlyEmit that if set would emit a set number of times in the Emitter Update call.
#279
That's unexpected, why is the mpeg audio decoding code disabled there? This means the only type of audio track supported is strictly ogg - additionally, the libraries can't be the issue since they are the same libraries used by VLC.
#280
The sound decoding in AGS implementation is not done from libogg, but instead using SDL_Sound, which has a different implementation inside. This is done because we do decode the video images through the traditional Xiph library but pass the sound through the AGS sound system - this is for better sound control in our system, not in ags4 we have decoding multiple videos and other things supported there for video.

Anyway, my guess is you are hitting something that can be an issue in SDL_Sound, if it would be possible to get the video file, I could split the audio through a different program and run on SDL_Sound alone (without AGS) to try to figure it out what is happening.
SMF spam blocked by CleanTalk