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

Topics - eri0o

#1
Hi, I have recently found a small piece of software I wanted to experiment with for ideas. Problem is, it's from 1986 and for Mac computer of the time.

The software is World Builder. Does anyone knows a way I could run and play with this software?

https://www.macintoshrepository.org/2984-world-builder
#2
General Discussion / Fake 3D in 2D
Thu 05/10/2023 00:51:25
Just because I love these 3D but in 2D stuff - mostly because aesthetics and to see the tricks. I can't try to implement all methods, so better to at least aggregate somewhere.

This post is recent and the technique here is incredible

https://dioragame.com/devlog/?log=3

It accomplishes 3D in 2D on PlayDate - an underpowered gaming device with a black and white display.
#3
toaster version 0.1.2

Get Latest Release toaster.scm | GitHub Repo | Download project .zip

This is a initial version of Toaster, a module to produce Toasts. These are those messages that appear on the screen and fade.

I will eventually write better docs and make the module more interesting, but here is a demo to explain what it does.



Usage

The basic usage is simply Toaster t; t.Toast("A Toast!");



Script API

Toaster

A toaster can produce toasts, but you can configure your toaster to produce different toasts as desired!

eToastColor Toaster.BackgroundColor
Background color of the toast, in AGS Color.

TextWindowGUI* Toaster.TextWindowGUI
You can set a TextWindowGUI to put the toast in a text box.

FontType Toaster.Font
The font to write the toast message.

eToastTweenEasingType Toaster.SlideInEasing
The easing to use when moving the toast into screen. These are the same possible ones in the tween module!

eToastTweenEasingType Toaster.SlideOutEasing
The easing to use when moving the toast out of existence

eToastAlignement Toaster.OriginAlignment
From where to position the toasts. It can be on the left, center and right.

float Toaster.Duration
How long in seconds should the Toast be on-screen.

float Toaster.Rotation
Rotation in degrees for produced toasts.

int Toaster.Icon
Add sprite to use as an icon in the left corner of the message.



License
This module is created by eri0o is provided with MIT License, see LICENSE for more details. It uses easing code based on Edmundo Ruiz and Robert Penner's, works, which are MIT and BSD licensed, respectively (and included in the module script).
#4
I have been thinking on the design of room CRM files.

For some reason room files uses sprites stored in itself instead of reading the spritefile, these are the masks (Walkable areas, Regions, Hotspots and Walkbehinds) and the room backgrounds (up to 5). My guess is this is made so it's easy to share them and directly use them as room templates.

Is it better for resource management in the engine side to keep these in the room file instead of the spritefile (you never need the background/mask from one room in the other room)?

Now for a more practical question.

Recently Deflate was added as a compression option for sprites. Let's say I select deflate in the general setting. Are the sprites in the room files using deflate or do they use something else? Is it possible to detect if any of these room sprites is just empty black with all zeroes and instead store a hint there?
#5
Editor Development / Small editor improvements
Sun 17/09/2023 14:03:32
Occasionally when using the Editor I see some stuff that looks "maybe not that hard" and that feels it would improve usage. Whenever I attempt something in this tiny scope I will put here just to not polute elsewhere with tiny changes.

The other reason is just if I am thinking wrong about it someone can just interject here (assuming most people don't follow things in GitHub).

OK, today my first thing in this is adding a button to open the file explorer in the compiled directory.





https://github.com/adventuregamestudio/ags/pull/2133

#6
I notice in my either script intensive games, or very lots of dialog games, there is a lot of the size of the game that is mainly due to the size of the scripts - I usually work in low resolution, so the image files get very small already.

I am not sure how to do or proceed with this. On one hand in the past games usually compressed their texts using some strategy - this is really common in very old games that had to fit in a ROM cartridge with very limited storage. This is an strategy, to simply compress only the strings, but I wonder if there is any way to directly compress the entirety of the script objects.

I don't know if this is the best approach or some other approach is best - say have the packaging format itself support compression. I also don't know if it's reasonable to just decompress on game boot and have the uncompressed script objects to work with or if it would need to be streamed from the compressed package.

Anyway, because of the unknowns around this I decided to write it here just to start to think about this.
#7
Editor Development / AGS Editor on macOS
Fri 26/05/2023 21:33:32
So I am on macOS Ventura with a Mac mini 2023 with m2 chip.

AGS Editor depends on .NET Framework 4.6 and installing it on a recent macOS requires wine 8.3 or above, which aren't stable yet, meaning one can't install them with Homebrew yet.

So if you want to run on Wine the only current way of doing outside of building wine by hand is using Crossover - I used the trial version for testing.

On Crossover, I created a bottle (the way it names a wine prefix) where I installed .NET 4.0, .NET 4.5.2 and then .NET 4.6.2 (had to use the offline installer). I already had Rosetta enabled too, not sure what happens when one doesn't have it. After this I run the AGS Editor installer from the latest 3.6.0.48 version in the bottle using crossover interface.

After the install it looks like AGS Editor runs normally, I have only built for windows and web with it. Differently from my previous Linux experience, it looks like I can press F5 and debug an AGS game, so not sure if something was fixed in Wine in meanwhile or the wine build for macOS had no issue with this.

I will update here some months from now when a new wine version is stable.
#8
Following the new pointers in managed structs PR, from CW, I decided to give a shot at something that uses lots of pointers just to see what would happen.

So, from the original box2d lite code, from the original erin catto presentation, I attempted a port to AGS Script, just to see what would happen.



b2dlite.ash
Spoiler
Code: ags
// new module header
#define FLT_MAX 340282346638528859811704183484516925440.0
#define MAX_POINTS 32
#define MAX_ARBITERS 16
#define MAX_BODIES 256
#define MAX_JOINTS 256
#define MAX_CONTACTS 2

managed struct Vec2
{
	float x, y;

  import static Vec2* New(float x, float y); // $AUTOCOMPLETESTATICONLY$
 	import void Set(float x, float y);
  import void SetV(Vec2* v);
  import Vec2* Abs();
  import Vec2* Negate();
	import Vec2* Minus(Vec2* vb);
  import Vec2* Plus(Vec2* vb);
  import Vec2* Scale(float a);
  import float Length();
  import float Dot(Vec2* b);
  import float Cross(Vec2* b);
};

managed struct Mat22
{
  float col1_x, col1_y, col2_x, col2_y;
  
  import static Mat22* New(float col1_x, float col1_y, float col2_x, float col2_y); // $AUTOCOMPLETESTATICONLY$
  import static Mat22* NewFromAngle(float angle); // $AUTOCOMPLETESTATICONLY$
  import static Mat22* NewFromVec2(Vec2* col1, Vec2* col2); // $AUTOCOMPLETESTATICONLY$
  import Mat22* Transpose();
  import Mat22* Invert();
  import Mat22* Plus(Mat22* B);
  import Mat22* Multiply(Mat22* B);
  import Mat22* Abs();
};

managed struct Body
{
  import static Body* Create();
	float position_x, position_y;
	float rotation;

	float velocity_x, velocity_y;
	float angularVelocity;

	float force_x, force_y;
	float torque;

	float width_x, width_y;

	float friction;
	float mass, invMass;
	float I, invI;
    
	import void Set(Vec2* w, float m);
	import void AddForce(const Vec2* f);
};

managed struct Joint
{
  import static Joint* Create();
	Mat22* M;
	Vec2* localAnchor1, localAnchor2;
	Vec2* r1, r2;
	Vec2* bias;
	Vec2* P;		// accumulated impulse
	Body* body1;
	Body* body2;
	float biasFactor;
	float softness;
  
	import void Set(Body* body1, Body* body2, Vec2* anchor);

	import void PreStep(float inv_dt);
	import void ApplyImpulse();
};

managed struct FeaturePair
{
  static import FeaturePair* Create();
  
  char inEdge1;
  char outEdge1;
  char inEdge2;
  char outEdge2;
  import attribute int value;
  import int get_value();
  import void set_value(int value);
};

managed struct Contact
{
  static import Contact* Create();
  
	Vec2* position;
	Vec2* normal;
	Vec2* r1, r2;
	float separation;
	float Pn;	// accumulated normal impulse
	float Pt;	// accumulated tangent impulse
	float Pnb;	// accumulated normal impulse for position bias
	float massNormal, massTangent;
	float bias;
	FeaturePair* feature;
  import Contact* CopyTo(Contact* c);
};

managed struct Arbiter
{
  static import Arbiter* Create(Body* b1, Body* b2);
  
  import void Update(Contact* contacts[],  int numContacts);
  
	import void PreStep(float inv_dt);
	import void ApplyImpulse();

	Contact* contacts[];
	int numContacts;

	Body* body1;
	Body* body2;

	// Combined friction
	float friction;
};

managed struct Arbiters
{
  import void Add(Arbiter* arb);
  import void Remove(Body* b1, Body* b2);
  import Arbiter* Get(Body* b1, Body* b2);
  import void Clear();
  
  Arbiter* a[MAX_ARBITERS];
  int a_count;
  import static Arbiters* Create();
};

struct World
{
  import void Init(float gravity_x, float gravity_y, int iterations = 10);
	import void AddBody(Body* body);
	import void AddJoint(Joint* joint);
	import void Clear();

	import void Step(float dt);

	import void BroadPhase();

	Body* bodies[MAX_BODIES];
	int body_count;
  Joint* joints[MAX_JOINTS];
  int joint_count;
	Arbiters* arbiters;
  
	Vec2* gravity;
	int iterations;
	static import attribute bool accumulateImpulses;
  static import bool get_accumulateImpulses();
  static import void set_accumulateImpulses(bool value);
	static import attribute bool warmStarting;
  static import bool get_warmStarting();
  static import void set_warmStarting(bool value);
	static import attribute bool positionCorrection;
  static import bool get_positionCorrection();
  static import void set_positionCorrection(bool value);
};
[close]

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

// assertion check utility
void assert(bool expr)
{
  if (expr == false) AbortGame("Failed assertion!");
}

float _abs(float a)
{
  if (a >= 0.0)
    return a;
  return -a;
}

#region MATH_UTILITIES
static Vec2* Vec2::New(float x, float y) {
  Vec2* v = new Vec2;
  v.x = x;
  v.y = y;
  return v;
}
void  Vec2::Set(float x, float y) { this.x = x;  this.y = y; }
void  Vec2::SetV(Vec2* v) { this.x = v.x;  this.y = v.y; }
Vec2* Vec2::Negate() { return this.New(-this.x, -this.y); }
Vec2* Vec2::Abs() { return this.New(_abs(this.x), _abs(this.y)); }
Vec2* Vec2::Minus(Vec2* v) { return this.New(this.x - v.x, this.y - v.y); }
Vec2* Vec2::Plus(Vec2* v) { return this.New(this.x + v.x, this.y + v.y); }
Vec2* Vec2::Scale(float a) { return this.New(this.x * a, this.y * a); }
float Vec2::Length(){ return Maths.Sqrt(this.x*this.x + this.y*this.y); }
float Vec2::Dot(Vec2* b) { return this.x * b.x + this.y * b.y; }
float Vec2::Cross(Vec2* b){  return this.x * b.y - this.y * b.x; }

static Mat22* Mat22::New(float col1_x, float col1_y, float col2_x, float col2_y)
{
  Mat22* m = new Mat22;
  m.col1_x = col1_x; m.col2_x = col2_x;
  m.col1_y = col1_y; m.col2_y = col2_y;
  return m;
}

static Mat22* Mat22::NewFromAngle(float angle)
{
  float c = Maths.Cos(angle), s = Maths.Sin(angle);
  return Mat22.New(c, s, -s, c);
}
  
static Mat22* Mat22::NewFromVec2(Vec2* col1, Vec2* col2)
{
  return Mat22.New(col1.x, col1.y, col2.x, col2.y);
}

Mat22* Mat22::Transpose() 
{
  return this.New(this.col1_x, this.col2_x, this.col1_y, this.col2_y);
}

Mat22* Mat22::Invert()
{
  float a = this.col1_x, b = this.col2_x, c = this.col1_y, d = this.col2_y;
  float det = a * d - b * c;
  assert(det != 0.0);
  det = 1.0 / det;
  return this.New(det * d, -det * c, -det * b, det * a);
}

Mat22* Mat22::Plus(Mat22* B)
{
  return this.New(this.col1_x + B.col1_x, this.col1_y + B.col1_y, this.col2_x + B.col2_x, this.col2_y + B.col2_y);
}

Mat22* Mat22::Abs()
{
  return this.New(_abs(this.col1_x), _abs(this.col1_y), _abs(this.col2_x), _abs(this.col2_y));
}

Vec2* CrossV2S(Vec2* a, float s)
{
  return Vec2.New(s * a.y, -s * a.x);
}

Vec2* CrossS2V(float s, Vec2* a)
{
  return Vec2.New(-s * a.y, s * a.x);
}

Vec2* MultiplyVec2(Mat22* A, Vec2* v)
{
  return Vec2.New(A.col1_x * v.x + A.col2_x * v.y, A.col1_y * v.x + A.col2_y * v.y);
}

Mat22* Mat22::Multiply(Mat22* matb)
{
   //A * B.col1, A * B.col2);
  float m11 = this.col1_x * matb.col1_x + this.col2_x * matb.col1_y;
  float m12 = this.col1_y * matb.col1_x + this.col2_y * matb.col1_y;
  
  float m21 = this.col1_x * matb.col2_x + this.col2_x * matb.col2_y;
  float m22 = this.col1_y * matb.col2_x + this.col2_y * matb.col2_y;
  return this.New(m11, m12, m21, m22);
}

float _sign(float a)
{
  if(a < 0.0) return -1.0;
  return 1.0;
}

int _maxi(int a, int b)
{
  if (a > b)
    return a;
  return b;
}

int _mini(int a, int b)
{
  if (a < b)
    return a;
  return b;
}

int _clampi(int v, int min, int max)
{
  return _mini(max, _maxi(v, min));
}

float _max(float a, float b)
{
  if (a > b)
    return a;
  return b;
}

float _min(float a, float b)
{
  if (a < b)
    return a;
  return b;
}

float _clamp(float v, float min, float max)
{
  return _min(max, _max(v, min));
}

// Random number in range [-1,1]
float _Random()
{
  float r = IntToFloat(Random(100000));
  r /= 100000.0;
  return 2.0 * r - 1.0;
}

float _RandomRange(float lo, float hi)
{
  float r = IntToFloat(Random(100000));
  r /= 100000.0;
  return (hi - lo) * r + lo;
}
#endregion //MATH_UTILITIES

bool _accumulateImpulses;
bool _warmStarting;
bool _positionCorrection;

static bool World::get_accumulateImpulses()
{
  return _accumulateImpulses;
}

static void World::set_accumulateImpulses(bool value)
{
  _accumulateImpulses = value;
}

static bool World::get_warmStarting()
{
  return _warmStarting;
}

static void World::set_warmStarting(bool value)
{
  _warmStarting = value;
}

static bool World::get_positionCorrection()
{
  return _positionCorrection;
}

static void World::set_positionCorrection(bool value)
{
  _positionCorrection = value;
}

void FeaturePair::set_value(int v)
{
  this.inEdge1 = (v >> 24) & 0xff;
  this.inEdge2 = (v >> 16) & 0xff;
  this.outEdge1 = (v >> 8) & 0xff;
  this.outEdge2 = v & 0xff;
}

int FeaturePair::get_value()
{
  return (this.inEdge1 << 24) & 0xff000000 | (this.inEdge2 << 16) & 0x00ff0000 | (this.outEdge1 << 8) & 0x0000ff00 | this.outEdge2;
}

static FeaturePair* FeaturePair::Create()
{
  FeaturePair* fp = new FeaturePair;
  fp.inEdge1 = 0;
  fp.inEdge2 = 0;
  fp.outEdge1 = 0;
  fp.outEdge2 = 0;
  return fp;
}

void World::AddBody(Body* body)
{
  this.bodies[this.body_count] = body;
  this.body_count++;
}

void World::AddJoint(Joint* joint)
{
  this.joints[this.joint_count] = joint;
  this.joint_count++;
}


void Arbiters::Add(Arbiter* arb)
{
  if(this.a_count < MAX_ARBITERS) {
    this.a[this.a_count] = arb;
    this.a_count++;
  }
}

Contact* Contact::CopyTo(Contact* c)
{
  c.bias = this.bias;
  if(c.feature == null) {
    c.feature = FeaturePair.Create();
  }
  
  c.feature.inEdge1 = this.feature.inEdge1;
  c.feature.inEdge2 = this.feature.inEdge2;
  c.feature.outEdge1 = this.feature.outEdge1;
  c.feature.outEdge2 = this.feature.outEdge2;
  
  c.massNormal = this.massNormal;
  c.massTangent = this.massTangent;
  if(c.normal == null) {
    c.normal = Vec2.New(this.normal.x, this.normal.y);
  } else {
    c.normal.Set(this.normal.x, this.normal.y);  
  }
  
  c.Pn = this.Pn;
  c.Pnb = this.Pnb;
  if(c.position == null) {
    c.position = Vec2.New(c.position.x, c.position.y);
  } else {
    c.position.Set(c.position.x, c.position.y);
  }
  c.Pt = this.Pt;
  if(c.r1 == null) {
    c.r1 = Vec2.New(this.r1.x, this.r1.y);
  } else {
    c.r1.Set(this.r1.x, this.r1.y);
  }
  if(c.r2 == null) {
    c.r2 = Vec2.New(this.r2.x, this.r2.y);
  } else {
    c.r2.Set(this.r2.x, this.r2.y);
  }
  c.separation = this.separation;
  return c;
}

void Arbiters::Remove(Body* b1, Body* b2)
{
  int j=0;
  int removed = 0;
  for(int i=0; i<this.a_count; i++)
  {
    if(this.a[i].body1 == b1 && this.a[i].body2 == b2 ||
       this.a[i].body1 == b2 && this.a[i].body2 == b1 ) {
      removed++;
      continue;
    }
    this.a[j]= this.a[i];
    j++;
  }
  this.a_count-=removed;
}

Arbiter* Arbiters::Get(Body* b1, Body* b2)
{
  for(int i=0; i<this.a_count; i++)
  {
    if(this.a[i].body1 == b1 && this.a[i].body2 == b2 || 
       this.a[i].body1 == b2 && this.a[i].body2 == b1 ) {
      return this.a[i];
    }
  }
  return null;  
}

void Arbiters::Clear()
{
  for(int i=0; i<this.a_count; i++)
  {
    this.a[i] = null;
  }
  this.a_count = 0;
}

static Arbiters* Arbiters::Create()
{
  Arbiters* a = new Arbiters;
  return a;  
}

void World::Clear()
{
  for(int i=0; i<this.body_count; i++)
  {
    this.bodies[i] = null;
  }
  this.body_count = 0;
  
  for(int i=0; i<this.joint_count; i++)
  {
    this.joints[i] = null;
  }
  this.joint_count = 0;
  if(this.arbiters != null) {
    this.arbiters.Clear();
  } else {
    this.arbiters = Arbiters.Create();
  }
}

static Body* Body::Create()
{
  Body* b = new Body;
  b.position_x = 0.0;
  b.position_y = 0.0;
  b.rotation = 0.0;
  b.velocity_x = 0.0;
  b.velocity_y = 0.0;
  b.angularVelocity = 0.0;
  b.force_x = 0.0;
  b.force_y = 0.0;
  b.torque = 0.0;
  b.friction = 0.2;
  
  b.width_x = 1.0;
  b.width_y = 1.0;
  b.mass = FLT_MAX;
  b.invMass = 0.0;
  b.I = FLT_MAX;
  b.invI = 0.0;
  
  return b;
}

void Body::Set(Vec2* w, float m)
{
  this.position_x = 0.0;
  this.position_y = 0.0;
  this.rotation = 0.0;
  this.velocity_x = 0.0;
  this.velocity_y = 0.0;
  this.angularVelocity = 0.0;
  this.force_x = 0.0;
  this.force_y = 0.0;
  this.torque = 0.0;
  this.friction = 0.2;
  
  this.width_x = w.x;
  this.width_y = w.y;
  this.mass = m;

  if (this.mass < FLT_MAX)
  {
    this.invMass = 1.0 / this.mass;
    this.I = this.mass * (this.width_x * this.width_x + this.width_y * this.width_y) / 12.0;
    this.invI = 1.0 / this.I;
  }
  else
  {
    this.invMass = 0.0;
    this.I = FLT_MAX;
    this.invI = 0.0;
  }
}

static Joint* Joint::Create()
{
  Joint* jo = new Joint;
  jo.body1 = null;
  jo.body2 = null;
  
  jo.P = Vec2.New(0.0, 0.0);
  jo.biasFactor = 0.2;
  jo.softness = 0.0;
  
  jo.M = Mat22.New(0.0, 0.0, 0.0, 0.0);
  jo.r1 = Vec2.New(0.0, 0.0);
  jo.r2 = Vec2.New(0.0, 0.0);
  jo.localAnchor1 = Vec2.New(0.0, 0.0);
  jo.localAnchor2 = Vec2.New(0.0, 0.0);
  jo.bias = Vec2.New(0.0, 0.0);
  
  return jo;
}

void Joint::Set(Body* b1, Body* b2, Vec2* anchor)
{
  this.body1 = b1;
  this.body2 = b2;

  Mat22* Rot1 = Mat22.NewFromAngle(this.body1.rotation);
  Mat22* Rot2 = Mat22.NewFromAngle(this.body2.rotation);
  Mat22* Rot1T = Rot1.Transpose();
  Mat22* Rot2T = Rot2.Transpose();

  this.localAnchor1 = MultiplyVec2(Rot1T, anchor.Minus(Vec2.New(this.body1.position_x, this.body1.position_y)));
  this.localAnchor2 = MultiplyVec2(Rot2T, anchor.Minus(Vec2.New(this.body2.position_x, this.body2.position_y)));

  if(this.P == null) {
    this.P = Vec2.New(0.0, 0.0);
  } else {
    this.P.Set(0.0, 0.0);
  }

  this.softness = 0.0;
  this.biasFactor = 0.2;
}

void Joint::PreStep(float inv_dt)
{
  // Pre-compute anchors, mass matrix, and bias.
  Mat22* Rot1 = Mat22.NewFromAngle(this.body1.rotation);
  Mat22* Rot2 = Mat22.NewFromAngle(this.body2.rotation);

  this.r1 = MultiplyVec2(Rot1, this.localAnchor1);
  this.r2 = MultiplyVec2(Rot2, this.localAnchor2);

  // deltaV = deltaV0 + K * impulse
  // invM = [(1/m1 + 1/m2) * eye(2) - skew(r1) * invI1 * skew(r1) - skew(r2) * invI2 * skew(r2)]
  //      = [1/m1+1/m2     0    ] + invI1 * [r1.y*r1.y -r1.x*r1.y] + invI2 * [r1.y*r1.y -r1.x*r1.y]
  //        [    0     1/m1+1/m2]           [-r1.x*r1.y r1.x*r1.x]           [-r1.x*r1.y r1.x*r1.x]
  Mat22* K1 = Mat22.New(
    this.body1.invMass + this.body2.invMass, 0.0,
    0.0, this.body1.invMass + this.body2.invMass);

  Mat22* K2 = Mat22.New( this.body1.invI * this.r1.y * this.r1.y, -this.body1.invI * this.r1.x * this.r1.y, 
                        -this.body1.invI * this.r1.x * this.r1.y,  this.body1.invI * this.r1.x * this.r1.x);
  
  
  Mat22* K3 = Mat22.New(this.body2.invI * this.r2.y * this.r2.y, -this.body2.invI * this.r2.x * this.r2.y,
                       -this.body2.invI * this.r2.x * this.r2.y,   this.body2.invI * this.r2.x * this.r2.x);

  Mat22* K = K1.Plus(K2);
  K = K.Plus(K3);

  K.col1_x += this.softness;
  K.col1_y += this.softness;

  this.M = K.Invert();

  Vec2* p1 = this.r1.Plus(Vec2.New(this.body1.position_x, this.body1.position_y));
  Vec2* p2 = this.r2.Plus(Vec2.New(this.body2.position_x, this.body2.position_y));
  Vec2* dp = p2.Minus(p1);
   
  if (_positionCorrection)
  {
    this.bias = dp.Scale(- this.biasFactor * inv_dt);
  }
  else
  {
    if(this.bias == null) {
      this.bias = Vec2.New(0.0, 0.0);
    } else {
      this.bias.Set(0.0, 0.0);
    }
  }

  if (_warmStarting)
  {
    // Apply accumulated impulse.
    this.body1.velocity_x -= this.body1.invMass * this.P.x;
    this.body1.velocity_y -= this.body1.invMass * this.P.y;
    this.body1.angularVelocity -= this.body1.invI * this.r1.Cross(this.P);
    
    this.body2.velocity_x += this.body2.invMass * this.P.x;
    this.body2.velocity_y += this.body2.invMass * this.P.y;
    this.body2.angularVelocity += this.body2.invI * this.r2.Cross(this.P);
  }
  else
  {
    if(this.P == null) {
      this.P = Vec2.New(0.0, 0.0);
    } else {
      this.P.Set(0.0, 0.0);
    }
  }
}

void Joint::ApplyImpulse()
{
  Vec2* tmpa = CrossS2V(this.body2.angularVelocity, this.r2);
  tmpa.x += this.body2.velocity_x;
  tmpa.y += this.body2.velocity_y;
  tmpa.x -= this.body1.velocity_x;
  tmpa.y -= this.body1.velocity_y;
  
  Vec2* tmpb = CrossS2V(this.body1.angularVelocity, this.r1);
  
  Vec2* dv = tmpa.Minus(tmpb);

  Vec2* tmp = this.bias.Minus(dv);
  tmp = tmp.Minus(this.P.Scale(this.softness));
  Vec2* impulse = MultiplyVec2(this.M, tmp);

  this.body1.velocity_x -= this.body1.invMass*impulse.x;
  this.body1.velocity_y -= this.body1.invMass*impulse.y;
  this.body1.angularVelocity -= this.body1.invI * this.r1.Cross(impulse);
  
  
  this.body2.velocity_x += this.body2.invMass*impulse.x;
  this.body2.velocity_y += this.body2.invMass*impulse.y;
  this.body2.angularVelocity += this.body2.invI * this.r2.Cross(impulse);

  this.P = this.P.Plus(impulse);
}

// Box vertex and edge numbering:
//
//        ^ y
//        |
//        e1
//   v2 ------ v1
//    |        |
// e2 |        | e4  --> x
//    |        |
//   v3 ------ v4
//        e3


enum Axis
{
  FACE_A_X,
  FACE_A_Y,
  FACE_B_X,
  FACE_B_Y
};

enum EdgeNumbers
{
  NO_EDGE = 0,
  EDGE1,
  EDGE2,
  EDGE3,
  EDGE4
};

managed struct ClipVertex
{
  static import ClipVertex* Create();

  Vec2* v;
  FeaturePair* fp;
};


static ClipVertex* ClipVertex::Create()
{
  ClipVertex* cv = new ClipVertex;
  cv.v = Vec2.New(0.0, 0.0);
  cv.fp = FeaturePair.Create();
  return cv;
}

static Contact* Contact::Create()
{
  Contact* c = new Contact;
  c.bias = 0.0;
  c.feature = FeaturePair.Create();
  c.massNormal = 0.0;
  c.massTangent = 0.0;
  c.normal = Vec2.New(0.0, 0.0);
  c.Pn = 0.0;
  c.Pnb = 0.0;
  c.position = Vec2.New(0.0, 0.0);
  c.Pt = 0.0;
  c.r1 = Vec2.New(0.0, 0.0);
  c.r2 = Vec2.New(0.0, 0.0);
  c.separation = 0.0;
  return c;
}

void Flip(FeaturePair* fp)
{
  char tmp = fp.inEdge1;
  fp.inEdge1 = fp.inEdge2;
  fp.inEdge2 = tmp;
  
  tmp = fp.outEdge1; 
  fp.outEdge1 = fp.outEdge2;
  fp.outEdge2 = tmp;
}

int ClipSegmentToLine(ClipVertex* vOut[], ClipVertex* vIn[], Vec2* normal, float offset, char clipEdge)
{
  // Start with no output points
  int numOut = 0;

  // Calculate the distance of end points to the line
  float distance0 = normal.Dot(vIn[0].v) - offset;
  float distance1 = normal.Dot(vIn[1].v) - offset;

  // If the points are behind the plane
  if (distance0 <= 0.0) {
    vOut[numOut] = vIn[0];
    numOut++;
  }
  if (distance1 <= 0.0) {
    vOut[numOut] = vIn[1];
    numOut++;
  }

  // If the points are on different sides of the plane
  if (distance0 * distance1 < 0.0)
  {
    // Find intersection point of edge and plane
    float interp = distance0 / (distance0 - distance1);
    Vec2* tmp_v = vIn[1].v.Minus(vIn[0].v);
    tmp_v = tmp_v.Scale(interp);
    vOut[numOut].v = vIn[0].v.Plus(tmp_v);
    
    if (distance0 > 0.0)
    {
      vOut[numOut].fp = vIn[0].fp;
      vOut[numOut].fp.inEdge1 = clipEdge;
      vOut[numOut].fp.inEdge2 = NO_EDGE;
    }
    else
    {
      vOut[numOut].fp = vIn[1].fp;
      vOut[numOut].fp.outEdge1 = clipEdge;
      vOut[numOut].fp.outEdge2 = NO_EDGE;
    }
    numOut++;
  }

  return numOut;
}

void ComputeIncidentEdge(ClipVertex* c[], Vec2* h, Vec2* pos, Mat22* Rot, Vec2* normal)
{
  // The normal is from the reference box. Convert it
  // to the incident boxe's frame and flip sign.
  Mat22* RotT = Rot.Transpose();
  Vec2* n = MultiplyVec2(RotT, normal);
  n = n.Negate();
  Vec2* nAbs = n.Abs();

  if (nAbs.x > nAbs.y)
  {
    if (_sign(n.x) > 0.0)
    {
      c[0].v.Set(h.x, -h.y);
      c[0].fp.inEdge2 = EDGE3;
      c[0].fp.outEdge2 = EDGE4;

      c[1].v.Set(h.x, h.y);
      c[1].fp.inEdge2 = EDGE4;
      c[1].fp.outEdge2 = EDGE1;
    }
    else
    {
      c[0].v.Set(-h.x, h.y);
      c[0].fp.inEdge2 = EDGE1;
      c[0].fp.outEdge2 = EDGE2;

      c[1].v.Set(-h.x, -h.y);
      c[1].fp.inEdge2 = EDGE2;
      c[1].fp.outEdge2 = EDGE3;
    }
  }
  else
  {
    if (_sign(n.y) > 0.0)
    {
      c[0].v.Set(h.x, h.y);
      c[0].fp.inEdge2 = EDGE4;
      c[0].fp.outEdge2 = EDGE1;

      c[1].v.Set(-h.x, h.y);
      c[1].fp.inEdge2 = EDGE1;
      c[1].fp.outEdge2 = EDGE2;
    }
    else
    {
      c[0].v.Set(-h.x, -h.y);
      c[0].fp.inEdge2 = EDGE2;
      c[0].fp.outEdge2 = EDGE3;

      c[1].v.Set(h.x, -h.y);
      c[1].fp.inEdge2 = EDGE3;
      c[1].fp.outEdge2 = EDGE4;
    }
  }

  c[0].v = pos.Plus(MultiplyVec2(Rot, c[0].v));
  c[1].v = pos.Plus(MultiplyVec2(Rot, c[1].v));
}

// The normal points from A to B
int Collide(Contact* contacts[], Body* bodyA, Body* bodyB)
{
  // Setup
  Vec2* hA = Vec2.New(bodyA.width_x, bodyA.width_y);
  hA = hA.Scale(0.5);
  Vec2* hB = Vec2.New(bodyB.width_x, bodyB.width_y);
  hB = hB.Scale(0.5);

  Vec2* posA = Vec2.New(bodyA.position_x, bodyA.position_y);
  Vec2* posB = Vec2.New(bodyB.position_x, bodyB.position_y);

  Mat22* RotA = Mat22.NewFromAngle(bodyA.rotation);
  Mat22* RotB = Mat22.NewFromAngle(bodyB.rotation);
  
  Mat22* RotAT = RotA.Transpose();
  Mat22* RotBT = RotB.Transpose();

  Vec2* dp = posB.Minus(posA);
  Vec2* dA = MultiplyVec2(RotAT, dp);
  Vec2* dB = MultiplyVec2(RotBT, dp);

  Mat22* C = RotAT.Multiply(RotB);
  Mat22* absC = C.Abs();
  Mat22* absCT = absC.Transpose();

  // Box A faces - Vec2 faceA = Abs(dA) - hA - absC * hB;
  Vec2* faceA = dA.Abs();
  faceA = faceA.Minus(hA);
  faceA = faceA.Minus(MultiplyVec2(absC, hB));
  
  if (faceA.x > 0.0 || faceA.y > 0.0)
    return 0;

  // Box B faces - Vec2 faceB = Abs(dB) - hB - absCT * hA;
  Vec2* faceB = dB.Abs();
  faceB = faceB.Minus(hB);
  faceB = faceB.Minus(MultiplyVec2(absCT, hA));
  
  if (faceB.x > 0.0 || faceB.y > 0.0)
    return 0;

  // Find best axis
  Axis axis;
  float separation;
  Vec2* normal;

  // Box A faces
  axis = FACE_A_X;
  separation = faceA.x;
  if(dA.x > 0.0) {
    normal = Vec2.New(RotA.col1_x, RotA.col1_y);
  } else {
    normal = Vec2.New(-RotA.col1_x, -RotA.col1_y);
  }
  
  float relativeTol = 0.95;
  float absoluteTol = 0.01;

  if (faceA.y > relativeTol * separation + absoluteTol * hA.y)
  {
    axis = FACE_A_Y;
    separation = faceA.y;
    if(dA.y > 0.0) {
      normal.Set(RotA.col2_x, RotA.col2_y);
    } else {
      normal.Set(-RotA.col2_x, -RotA.col2_y);
    }
  }

  // Box B faces
  if (faceB.x > relativeTol * separation + absoluteTol * hB.x)
  {
    axis = FACE_B_X;
    separation = faceB.x;
    if(dB.x > 0.0) {
      normal.Set(RotB.col1_x, RotB.col1_y);
    } else {
      normal.Set(-RotB.col1_x, -RotB.col1_y);
    }
  }

  if (faceB.y > relativeTol * separation + absoluteTol * hB.y)
  {
    axis = FACE_B_Y;
    separation = faceB.y;
    if(dB.y > 0.0) {
      normal.Set(RotB.col2_x, RotB.col2_y);
    } else {
      normal.Set(-RotB.col2_x, -RotB.col2_y);
    }
  }

  // Setup clipping plane data based on the separating axis
  Vec2* frontNormal;
  Vec2* sideNormal;
  ClipVertex* incidentEdge[] = new ClipVertex[2];
  incidentEdge[0] = ClipVertex.Create();
  incidentEdge[1] = ClipVertex.Create();
  float front, negSide, posSide;
  char negEdge, posEdge;

  // Compute the clipping lines and the line segment to be clipped.
  switch (axis)
  {
  case FACE_A_X:
    {
      frontNormal = normal;
      front = posA.Dot(frontNormal) + hA.x;
      sideNormal = Vec2.New(RotA.col2_x, RotA.col2_y);
      float side = posA.Dot(sideNormal);
      negSide = -side + hA.y;
      posSide =  side + hA.y;
      negEdge = EDGE3;
      posEdge = EDGE1;
      ComputeIncidentEdge(incidentEdge, hB, posB, RotB, frontNormal);
    }
    break;

  case FACE_A_Y:
    {
      frontNormal = normal;
      front = posA.Dot(frontNormal) + hA.y;
      sideNormal = Vec2.New(RotA.col1_x, RotA.col1_y);
      float side = posA.Dot(sideNormal);
      negSide = -side + hA.x;
      posSide =  side + hA.x;
      negEdge = EDGE2;
      posEdge = EDGE4;
      ComputeIncidentEdge(incidentEdge, hB, posB, RotB, frontNormal);
    }
    break;

  case FACE_B_X:
    {
      frontNormal = normal.Negate();
      front = posB.Dot(frontNormal) + hB.x;
      sideNormal = Vec2.New(RotB.col2_x, RotB.col2_y);
      float side = posB.Dot(sideNormal);
      negSide = -side + hB.y;
      posSide =  side + hB.y;
      negEdge = EDGE3;
      posEdge = EDGE1;
      ComputeIncidentEdge(incidentEdge, hA, posA, RotA, frontNormal);
    }
    break;

  case FACE_B_Y:
    {
      frontNormal = normal.Negate();
      front = posB.Dot(frontNormal) + hB.y;
      sideNormal = Vec2.New(RotB.col1_x, RotB.col1_y);
      float side = posB.Dot(sideNormal);
      negSide = -side + hB.x;
      posSide =  side + hB.x;
      negEdge = EDGE2;
      posEdge = EDGE4;
      ComputeIncidentEdge(incidentEdge, hA, posA, RotA, frontNormal);
    }
    break;
  }

  // clip other face with 5 box planes (1 face plane, 4 edge planes)

  ClipVertex* clipPoints1[] = new ClipVertex[2];
  clipPoints1[0] = ClipVertex.Create();
  clipPoints1[1] = ClipVertex.Create();
  ClipVertex* clipPoints2[] = new ClipVertex[2];
  clipPoints2[0] = ClipVertex.Create();
  clipPoints2[1] = ClipVertex.Create();
  int np;

  // Clip to box side 1
  np = ClipSegmentToLine(clipPoints1, incidentEdge, sideNormal.Negate(), negSide, negEdge);

  if (np < 2)
    return 0;

  // Clip to negative box side 1
  np = ClipSegmentToLine(clipPoints2, clipPoints1,  sideNormal, posSide, posEdge);

  if (np < 2)
    return 0;

  // Now clipPoints2 contains the clipping points.
  // Due to roundoff, it is possible that clipping removes all points.

  int numContacts = 0;
  for (int i = 0; i < 2; i++)
  {
    float sep = frontNormal.Dot(clipPoints2[i].v) - front;

    if (sep <= 0.0)
    {
      contacts[numContacts].separation = sep;
      contacts[numContacts].normal = normal;
      // slide contact point onto reference face (easy to cull)
      contacts[numContacts].position.SetV(clipPoints2[i].v.Minus(frontNormal.Scale(sep)));
      contacts[numContacts].feature = clipPoints2[i].fp;
      if (axis == FACE_B_X || axis == FACE_B_Y)
        Flip(contacts[numContacts].feature);

      numContacts++;
    }
  }

  return numContacts;
}


static Arbiter* Arbiter::Create(Body* b1, Body* b2)
{
  Arbiter* a = new Arbiter;
 
  if(b1.mass < FLT_MAX) {
  a.body1 = b1;
  a.body2 = b2;
  } else {
  a.body1 = b2;
  a.body2 = b1;
  }
  
  a.contacts = new Contact[MAX_CONTACTS];
  for(int i=0; i<MAX_CONTACTS; i++) {
    a.contacts[i] = Contact.Create();
  }

  a.numContacts = Collide(a.contacts, a.body1, a.body2);

  a.friction = Maths.Sqrt(a.body1.friction * a.body2.friction);
  return a;
}

void Arbiter::Update(Contact* newContacts[], int numNewContacts)
{
  Contact* mergedContacts[2];
  mergedContacts[0] = Contact.Create();
  mergedContacts[1] = Contact.Create();

  for (int i=0; i < numNewContacts; i++)
  {
    Contact* cNew = newContacts[i];
    int k = -1;
    for (int j=0; j < this.numContacts; j++)
    {
      Contact* cOld = this.contacts[j];
      if (cNew.feature.inEdge1 == cOld.feature.inEdge1 &&
          cNew.feature.inEdge2 == cOld.feature.inEdge2 &&
          cNew.feature.outEdge1 == cOld.feature.outEdge1 &&
          cNew.feature.outEdge2 == cOld.feature.outEdge2 )
      {
        k = j;
        break;
      }
    }

    if (k > -1)
    {
      Contact* cOld = this.contacts[k];
      mergedContacts[i] = cNew;
      if (_warmStarting)
      {
        cNew.Pn = cOld.Pn;
        cNew.Pt = cOld.Pt;
        cNew.Pnb = cOld.Pnb;
      }
      else
      {
        cNew.Pn = 0.0;
        cNew.Pt = 0.0;
        cNew.Pnb = 0.0;
      }
    }
    else
    {
      mergedContacts[i] = newContacts[i];
    }
  }

  for (int i = 0; i < numNewContacts; i++)
    this.contacts[i] = mergedContacts[i];

  this.numContacts = numNewContacts;
}


void Arbiter::ApplyImpulse()
{
  Body* b1 = this.body1;
  Body* b2 = this.body2;

  for (int i=0; i < this.numContacts; i++)
  {
    Contact* c = this.contacts[i];
    c.r1.x = c.position.x - b1.position_x;
    c.r1.y = c.position.y - b1.position_y;
    c.r2.x = c.position.x - b2.position_x;
    c.r2.y = c.position.y - b2.position_y;

    // Relative velocity at contact
    Vec2* tmpa = CrossS2V(b2.angularVelocity, c.r2);
    tmpa.x += b2.velocity_x;
    tmpa.y += b2.velocity_y;
    tmpa.x -= b1.velocity_x;
    tmpa.y -= b1.velocity_y;
    
    Vec2* tmpb = CrossS2V(b1.angularVelocity, c.r1);
    
    Vec2* dv = tmpa.Minus(tmpb);

    // Compute normal impulse
    float vn = dv.Dot(c.normal);

    float dPn = c.massNormal * (-vn + c.bias);

    if (_accumulateImpulses)
    {
      // Clamp the accumulated impulse
      float Pn0 = c.Pn;
      c.Pn = _max(Pn0 + dPn, 0.0);
      dPn = c.Pn - Pn0;
    }
    else
    {
      dPn = _max(dPn, 0.0);
    }

    // Apply contact impulse
    Vec2* Pn = c.normal.Scale(dPn);

    b1.velocity_x -= b1.invMass * Pn.x;
    b1.velocity_y -= b1.invMass * Pn.y;
    b1.angularVelocity -= b1.invI * c.r1.Cross(Pn);

    b2.velocity_x += b2.invMass * Pn.x;
    b2.velocity_y += b2.invMass * Pn.y;
    b2.angularVelocity += b2.invI * c.r2.Cross(Pn);
    
    // Relative velocity at contact
    
    tmpa = CrossS2V(b2.angularVelocity, c.r2);
    tmpa.x += b2.velocity_x;
    tmpa.y += b2.velocity_y;
    tmpa.x -= b1.velocity_x;
    tmpa.y -= b1.velocity_y;
    
    tmpb = CrossS2V(b1.angularVelocity, c.r1);
    
    dv = tmpa.Minus(tmpb);

    Vec2* tangent = CrossV2S(c.normal, 1.0);
    float vt = dv.Dot(tangent);
    float dPt = c.massTangent * (-vt);

    if (_accumulateImpulses)
    {
      // Compute friction impulse
      float maxPt = this.friction * c.Pn;

      // Clamp friction
      float oldTangentImpulse = c.Pt;
      c.Pt = _clamp(oldTangentImpulse + dPt, -maxPt, maxPt);
      dPt = c.Pt - oldTangentImpulse;// _clamp(c.Pt - oldTangentImpulse, -maxPt, maxPt);
    }
    else
    {
      float maxPt = this.friction * dPn;
      dPt = _clamp(dPt, -maxPt, maxPt);
    }

    // Apply contact impulse
    Vec2* Pt = tangent.Scale(dPt);

    b1.velocity_x -= b1.invMass * Pt.x;
    b1.velocity_y -= b1.invMass * Pt.y;
    b1.angularVelocity -= b1.invI * c.r1.Cross(Pt);

    b2.velocity_x += b2.invMass * Pt.x;
    b2.velocity_y += b2.invMass * Pt.y;
    b2.angularVelocity += b2.invI * c.r2.Cross(Pt);
  }
}

void Arbiter::PreStep(float inv_dt)
{
  float k_allowedPenetration = 0.01;
  float k_biasFactor = 0.0;
  if(_positionCorrection) k_biasFactor = 0.2;

  for (int i = 0; i < this.numContacts; i++)
  {
    Contact* c = this.contacts[i];

    Vec2* r1 = new Vec2;
    Vec2* r2 = new Vec2;
    
    r1.x = c.position.x - this.body1.position_x;
    r1.y = c.position.y - this.body1.position_y;
    r2.x = c.position.x - this.body2.position_x;
    r2.y = c.position.y - this.body2.position_y;

    // Precompute normal mass, tangent mass, and bias.
    float rn1 = r1.Dot(c.normal);
    float rn2 = r2.Dot(c.normal);
    float kNormal = this.body1.invMass + this.body2.invMass;
    kNormal += this.body1.invI * (r1.Dot(r1) - rn1 * rn1) + this.body2.invI * (r2.Dot(r2) - rn2 * rn2);
    c.massNormal = 1.0 / kNormal;

    Vec2* tangent = CrossV2S(c.normal, 1.0);
    float rt1 = r1.Dot(tangent);
    float rt2 = r2.Dot(tangent);
    float kTangent = this.body1.invMass + this.body2.invMass;
    kTangent += this.body1.invI * (r1.Dot(r1) - rt1 * rt1) + this.body2.invI * (r2.Dot(r2) - rt2 * rt2);
    c.massTangent = 1.0 /  kTangent;

    c.bias = -k_biasFactor * inv_dt * _min(0.0, c.separation + k_allowedPenetration);

    if (_accumulateImpulses)
    {
      // Apply normal + friction impulse
      Vec2* ppp = tangent.Scale(c.Pt);
      Vec2* pp = c.normal.Scale(c.Pn);
      Vec2* P = pp.Plus(ppp);
      pp = null; ppp = null;

      this.body1.velocity_x -= this.body1.invMass * P.x;
      this.body1.velocity_y -= this.body1.invMass * P.y;      
      this.body1.angularVelocity -= this.body1.invI * r1.Cross(P);

      this.body2.velocity_x += this.body2.invMass * P.x;
      this.body2.velocity_y += this.body2.invMass * P.y;
      this.body2.angularVelocity += this.body2.invI * r2.Cross(P);
    }
  }
}

void World::BroadPhase()
{
  // O(n^2) broad-phase
  for (int i = 0; i < this.body_count; i++)
  {
    Body* bi = this.bodies[i];

    for (int j = i + 1; j < this.body_count; j++)
    {
      Body* bj = this.bodies[j];

      if (bi.invMass == 0.0 && bj.invMass == 0.0)
        continue;

      Arbiter* newArb = Arbiter.Create(bi, bj);

      if (newArb.numContacts > 0)
      {
        Arbiter* arb = this.arbiters.Get(bi, bj);
        if (arb == null)
        {
          this.arbiters.Add(newArb);
        }
        else
        {
          arb.Update(newArb.contacts, newArb.numContacts);
        }
      }
      else
      {
        this.arbiters.Remove(bi, bj);
      }
    }
  }
}

void World::Step(float dt)
{
  float inv_dt = 0.0;
  if(dt > 0.0) {
    inv_dt = 1.0 / dt;
  }

  // Determine overlapping bodies and update contact points.
  this.BroadPhase();

  // Integrate forces.
  for (int i = 0; i < this.body_count; i++)
  {
    Body* b = this.bodies[i];

    if (b.invMass == 0.0)
      continue;

    b.velocity_x += dt * (this.gravity.x + b.invMass * b.force_x);
    b.velocity_y += dt * (this.gravity.y + b.invMass * b.force_y);
    b.angularVelocity += dt * b.invI * b.torque;
  }

  // Perform pre-steps. 
  for (int i=0; i< this.arbiters.a_count; i++)
  {
    Arbiter* arb = this.arbiters.a[i];
    arb.PreStep(inv_dt);
  }

  for (int i = 0; i < this.joint_count; i++)
  {
    this.joints[i].PreStep(inv_dt);
  }

  // Perform iterations
  for (int i=0; i < this.iterations; i++)
  {
    for (int j=0; j< this.arbiters.a_count; j++)
    {
      Arbiter* arb = this.arbiters.a[j];
      arb.ApplyImpulse();
    }

    for (int j=0; j < this.joint_count; j++)
    {
      this.joints[j].ApplyImpulse();
    }
  }

  // Integrate Velocities
  for (int i = 0; i < this.body_count; i++)
  {
    Body* b = this.bodies[i];

    b.position_x += dt * b.velocity_x;
    b.position_y += dt * b.velocity_y;
    b.rotation += dt * b.angularVelocity;

    b.force_x = 0.0;
    b.force_y = 0.0;
    b.torque = 0.0;
  }
}

void World::Init(float gravity_x, float gravity_y, int iterations)
{
  this.Clear();
  this.gravity = Vec2.New(gravity_x, gravity_y);
  this.iterations = iterations;
}
 
void game_start()
{
  _accumulateImpulses = true;
  _warmStarting = true;
  _positionCorrection = true;
}
[close]

Usage, room script example

Code: ags
// room script file
#define MAX_STUFF 32

#define SCALE 16.0

World world;
float step;
struct Thing {
  Body* b;
  Overlay* ovr;
  DynamicSprite* dnspr;
  
  import void InitFromBody(Body* b);
  import void Render();
};
Thing things[MAX_STUFF];
int thing_count;
Overlay* scr_ovr;
DynamicSprite* scr_spr;

void DrawContacts()
{
  if(scr_ovr == null) {
    scr_spr = DynamicSprite.Create(Screen.Width, Screen.Height);
    scr_ovr = Overlay.CreateGraphical(0, 0, scr_spr.Graphic);
    scr_ovr.ZOrder = 1000;
  }
  
  DrawingSurface* surf = scr_spr.GetDrawingSurface();
  surf.Clear(COLOR_TRANSPARENT);
  surf.DrawingColor = 61871;
  
  for(int arsi=0; arsi<world.arbiters.a_count; arsi++) {
    Arbiter* arb = world.arbiters.a[arsi];
    for(int i=0; i<arb.numContacts; i++) {
      int cnx = FloatToInt(arb.contacts[i].position.x*SCALE)+Screen.Width/2;
      int cny = FloatToInt(arb.contacts[i].position.y*SCALE)+Screen.Height/2;
      surf.DrawCircle(cnx, cny, 3);
    }  
  }
}

void Thing::InitFromBody(Body* b) {
  this.b = b;
  
  DrawingSurface* surf;
  int w = FloatToInt(b.width_x*SCALE);
  int h = FloatToInt(b.width_y*SCALE);
  this.dnspr = DynamicSprite.Create(w, h);
  surf = this.dnspr.GetDrawingSurface();
  surf.Clear(256 + Random(65504));
  if(w > 2 && h > 2) {
    surf.DrawingColor = 256 + Random(65504);
    surf.DrawRectangle(1, 1, w-2, h-2);
  }
  surf.Release();
  
  this.ovr = Overlay.CreateGraphical(0, 0, this.dnspr.Graphic);
  this.ovr.X = FloatToInt((this.b.position_x - this.b.width_x/2.0)*SCALE)+Screen.Width/2;
  this.ovr.Y = FloatToInt((this.b.position_y - this.b.width_y/2.0)*SCALE)+Screen.Height/2;
}

void Thing::Render() {
  int w = FloatToInt((this.b.width_x)*SCALE);
  int h = FloatToInt((this.b.width_y)*SCALE);
  this.ovr.Graphic = this.dnspr.Graphic;
  this.ovr.X = FloatToInt((this.b.position_x - this.b.width_x/2.0)*SCALE)+Screen.Width/2;
  this.ovr.Y = FloatToInt((this.b.position_y - this.b.width_y/2.0)*SCALE)+Screen.Height/2;
  this.ovr.Width = w;
  this.ovr.Height = h;
  this.ovr.Rotation = Maths.RadiansToDegrees(this.b.rotation);
}

void NewBody(float x, float y, float width, float height, float mass) {
  Body* b = Body.Create();
  b.Set(Vec2.New(width, height), mass);
  b.position_x = x;
  b.position_y = y;
  
  things[thing_count].InitFromBody(b);
  thing_count++;
	world.AddBody(b);
}

void RenderAll() {
  for(int i=0; i<thing_count; i++) {
    things[i].Render();
  }
  DrawContacts();
}

function room_Load()
{
  world.Init(0.0, 10.0);
  step = 1.0/IntToFloat(GetGameSpeed());
  
  NewBody(0.0, 0.5*20.0, 100.0, 20.0,   FLT_MAX);
  NewBody(0.0,       -4.0,   1.0,  1.0,   200.0);
  NewBody(1.0,       -6.0,   1.0,  1.0,   200.0);
  NewBody(2.0,       -5.0,   1.0,  1.0,   200.0);
  NewBody(1.5,       -2.0,   1.0,  1.0,   800.0);
  NewBody(2.4,       -9.0,   1.0,  1.0,   200.0);
  NewBody(1.4,       -10.0,   1.0,  1.0,   200.0);
  NewBody(2.8,       -12.0,   1.0,  1.0,   200.0);
  NewBody(1.8,       -14.0,   1.0,  1.0,   200.0);
  NewBody(0.8,       -12.0,   1.0,  1.0,   200.0);
}

bool start;
function room_RepExec()
{
  if(!start) {
    Display("START");
    start = !start;
  }
  
  world.Step(step);

  RenderAll();
}


Spoiler
at first it wasn't working...
[close]

It's working!!!

#9
A small plugin to open URL in a browser, made for AGS 3.6.0 and beyond. (the plugin uses SDL2, so if you are using a previous version of ags, you have to add SDL2 somewhere)

agsappopenurl_windows.zip | agsappopenurl_linux.tar.gzagsappopenurl_macos.zip

You call it like this:

Code: ags
AppOpenURL(eAUrlProto_https, "itch.io");

And it should open the itch website in a browser tab.

The idea is to use this plugin instead of the agsshell plugin - for safety reasons and better cross-platform support. It uses SDL2 behind the scenes, so it can't run in older versions of AGS. (well you would have to add SDL2 somewhere if using an old ags version)
#10
Hey, there have been some newcomer/bot/weird people digging some threads where the last post has been +5yr old.

I think when the thread itself (first post), it's alright to keep going, and maybe either some exception or update to the pinned posts. But for the rest, there is usually little sense to digging these really old threads - the moderator could be able to unlock, since we have some people that come back to their game after a long time.

Most of the time I have seen some post in these old threads it has been the spam bots that make some random post with a single link to whatever the website they are spamming. Maybe this could reduce being targeted by these boys a bit?
#11
So, I am trying to figure out how to travel between cities, and it appears Italy has a pretty good railway system.

But I can't understand the immense amount of different tickets, if I should get some type of pass instead of individual tickets, and if booking in advance is really needed - it seems a lot of different trains are available.

Somehow it's really hard to find good information online.  :-\
#12
Being able to playback videos in AGS while using GUIs at the same time could enable building FMV games in AGS. Bonus points for having the video just run in any Overlay (regular overlays or room overlays), which would allow something similar to using videos for things in the background or some specifics, like say, a communication device in a GUI that receives a message.

From what I remember, when a video is playing AGS enters in a special state, so the screen is exclusively for the video and it can't process other things beyond the key to skip the video.

This as other special states are distributed throughout the code (see: Refactor AGS for 1 game loop and game state managing). It looks like some states could be instead managed through a Finite State Machine, but others are things we kinda want to just happen at the same frame (so not exactly a different state). This refactoring is not necessary, but if there was some similar way to arrange this it could maybe work. Video is a bit tricky that the sound and image has to be synchronous, so not sure that could workout.

This thread is both to discuss use cases in game and to see what would be needed in ags details, and also figuring out things like the API for this.

#13
I just played this game with a friend right now, we were sitting in sofas one across another and relaying what each of us were seeing to the other. The Lite version is 20-30min, but there is a paid version that is longer. We didn't had much time so we played the lite and it's pretty interesting.

https://play.google.com/store/apps/details?id=air.com.RustyLake.ThePastWithinLite

It was a similar experience to Keep Talking and Nobody Explodes.

Just starting a thread to accumulate a bunch of little games that are interesting, in case it's useful for you know... Inspiring and making games. :)
#14
I found the abstract of the chapter of a book online that apparently mentions the AGS interface. Leaving here in case someone wants to read it.

https://link.springer.com/chapter/10.1007/978-3-031-05214-9_11

The book is named The Authoring Problem, and the chapter in question is named Mapping the Unmappable: Reimagining Visual Representations of Interactive Narrative (doi 10.1007/978-3-031-05214-9_11)

Unfortunately there's something wrong with my institutional access, so I haven't been able to read it yet.

Edit: Being onsite my access to springer normalized. There's not much in the article, AGS is in a context of other text based narrative development software, so in this context it's interface is much more visual - as others (Twine, Inform7 and Storyspace 3) focus more on text.

This article gave me an idea for a simple tool that naively grabs all player.ChangeRoom in the room scripts, notes down the script room number and the destination room, and then produces a small graph of the rooms and their connections. While I have my guesses to do this in either python, R or JS, I am not sure if there are useful graphing libraries in C# so it could be cooked up as an Editor plugin, which is more elegant.

(of course change rooms in the global script and other non-room scripts would be missed, but I guess this would still have a little use to check your story development)
#15
So, I wanted to see if it was possible to do Wave Function Collapse in AGS Script. Because I was lazy, I decided to try to port the code of this C version here: https://github.com/krychu/wfc/blob/master/wfc.h

Now this did NOT work, it runs and takes some long time, but the result at the end is wrong... Anyhow, decided to leave my try it out here in case someone really wants to take the challenge. Probably the best approach is to actually read the papers and figure a strategy that is better suited for the concepts available in AGS.

wfc.ash
Code: ags
// new module header

#define MAX_TILES 1024

enum WfcDirection {
  eWfcDir_Up = eDirectionUp, 
  eWfcDir_Down = eDirectionDown, 
  eWfcDir_Left = eDirectionLeft, 
  eWfcDir_Right = eDirectionRight 
};

enum WfcMethod {
  eWfc_Overlapping, 
  eWfc_Tiled, 
};

managed struct WfcCell {
  int Tile[MAX_TILES];                  // Possible tiles in the cell (initially all)
  int TileCount;

  int FrequencySum; // Sum of tile frequencies used to calculate
                    // entropy and randomly pick a tile when
                    // collapsing a tile.

  float Entropy;    // Shannon entropy. Cell with the smallest entropy
                    // is picked to be collapsed next.
};

managed struct WfcProp {
  int SrcCellIdx;
  int DstCellIdx;
  WfcDirection direction;
};

struct Wfc {
  WfcMethod method;
  int seed;
  
  DynamicSprite* Input;
  int TileWidth;
  int TileHeight;  
  bool FlipTilesX;
  bool FlipTilesY;
  bool RotateTiles;
  
  DynamicSprite* TilesSprite[];
  int TilesFrequency[]; // count of tile occurrences in the input image.
  int TilesCount;
  
  /* output */
  
  DynamicSprite* Output;
  WfcCell* Cell[];
  int CellCount;
  int FrequencySum;
  
  /* in-use */
  
  WfcProp* Prop[];
  int PropCount;
  int PropIdx;
  
  int CollapsedCellCount;
  
  bool allowed_tiles[1048576];
  
  
  /* API */
  
  import void Init(DynamicSprite* output, int input_sprite, int tile_width, int tile_height, bool xflip_tiles = true, bool yflip_tiles = true, bool rotate_tiles = true, bool expand_input = false);
  import void Restart();
  import void RegenerateOutput();
  import void Run(int max_collapse_cnt);
  import void RepeatedlyUpdate(int times = 1);
  bool Generated;
  
  protected int _Rp_CellIdx;
  protected int _Rp_MaxCollapseCnt;
  
  import protected void _SwapTiles(int a_idx, int b_idx);  
  import protected void _RemoveDuplicatedTiles();
  
  import protected void _CreateProps();
  import protected void _CreateCells();
  import protected void _CreateTiles();
  
  import protected void _AddProp(int src_cell_idx, int dst_cell_idx, WfcDirection direction);
  import protected void _AddPropUp(int src_cell_idx);
  import protected void _AddPropDown(int src_cell_idx);
  import protected void _AddPropLeft(int src_cell_idx);
  import protected void _AddPropRight(int src_cell_idx);
  import protected bool _IsTileEnabled(int tile_idx, int cell_idx, WfcDirection direction);
  import protected bool _IsPropPending(int cell_idx, WfcDirection direction);
  
  import protected bool _PropagateProp(WfcProp* p);
  import protected bool _Propagate(int cell_idx);
  import protected bool _Collapse(int cell_idx);
  import protected int _NextCell();
  import protected void _InitCells();
   
  import protected void _ComputeAllowedTiles();
  import protected void _CreateTilesOverlapping(int tile_width, int tile_height, bool expand_image, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles);
};

wfc.asc
Code: ags
// new module script

int _DirToIdx( WfcDirection direction)
{
  switch (direction) {
    case eWfcDir_Up:    return 0; break;
    case eWfcDir_Down:  return 1; break;
    case eWfcDir_Left:  return 2; break;
    case eWfcDir_Right: return 3; break;
  }
  return 0;
}

WfcDirection _IdxToDir(int d)
{
  switch (d) {
    case 0: return eWfcDir_Up; break;
    case 1: return eWfcDir_Down; break;
    case 2: return eWfcDir_Left; break;
    case 3: return eWfcDir_Right; break;
  }
  return eWfcDir_Up;
  
}

// Return 1 if the two images overlap perfectly except the edges in the given direction, 0 otherwise.
bool _SprCmpOverlap(DynamicSprite* dynspr_a, DynamicSprite* dynspr_b, WfcDirection direction)
{
  int a_offx, a_offy, b_offx, b_offy, width, height;

  switch (direction) {
    case eWfcDir_Up:
      a_offx = 0; a_offy = 0;
      b_offx = 0; b_offy = 1;
      width = dynspr_a.Width;
      height = dynspr_a.Height-1;
      break;
    case eWfcDir_Down:
      a_offx = 0; a_offy = 1;
      b_offx = 0; b_offy = 0;
      width = dynspr_a.Width;
      height = dynspr_a.Height-1;
      break;
    case eWfcDir_Left:
      a_offx = 0; a_offy = 0;
      b_offx = 1; b_offy = 0;
      width = dynspr_a.Width-1;
      height = dynspr_a.Height;
      break;
    case eWfcDir_Right:
      a_offx = 1; a_offy = 0;
      b_offx = 0; b_offy = 0;
      width = dynspr_a.Width-1;
      height = dynspr_a.Height;
      break;
    default:
      AbortGame("WFC Error: Invalid Direction");
      return false;
  }

  DrawingSurface* surf_a = dynspr_a.GetDrawingSurface();
  DrawingSurface* surf_b = dynspr_b.GetDrawingSurface();

  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {

      int a_x = a_offx + x;
      int b_x = b_offx + x;
      int a_y = a_offy + y;
      int b_y = b_offy + y;

      if(surf_a.GetPixel(a_x, a_y) != surf_b.GetPixel(b_x, b_y)) {
        return false;
      }
    }
  }
  
  surf_a.Release();
  surf_b.Release();
  
  return true;
}

 bool noloopcheck _SprEqual(DynamicSprite* dynspr_a, DynamicSprite* dynspr_b)
{
  if(dynspr_a.Width != dynspr_b.Width || dynspr_a.Height != dynspr_b.Height) {
    return false;
  }
  
  if(dynspr_a == dynspr_b) return true;
  
  int width = dynspr_a.Width;
  int height = dynspr_a.Height;
  
  DrawingSurface* surf_a = dynspr_a.GetDrawingSurface();
  DrawingSurface* surf_b = dynspr_b.GetDrawingSurface();
  
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      if(surf_a.GetPixel(x, y) != surf_b.GetPixel(x, y)) {
        return false;
      }
    }
  }
  
  return true;
}

DynamicSprite* noloopcheck _GenerateOutput(int width, int height,  WfcCell* cells[], DynamicSprite* tiles[])
{
  DynamicSprite* spr = DynamicSprite.Create(width, height);
  DrawingSurface* surf = spr.GetDrawingSurface();
  
  for (int y=0; y<height; y++) {
    for (int x=0; x<width; x++) {
      WfcCell* cell = cells[y*width+x];
      
      int component = 0;
      for(int i=0; i<cell.TileCount; i++) {
        DynamicSprite* tile = tiles[cell.Tile[i]];
        DrawingSurface* tmp_surf = tile.GetDrawingSurface();
        component += tmp_surf.GetPixel(0, 0);
        tmp_surf.Release();
      }
      
      surf.DrawingColor = component/cell.TileCount;
      surf.DrawPixel(x, y);
    }
  }
  
  surf.Release();
  return spr;
}

void _AddOverlappingImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], DynamicSprite* img, int xcnt, int ycnt, int tile_width, int tile_height)
{
  int tile_cnt = xcnt * ycnt;
  DrawingSurface* surf = img.GetDrawingSurface();
  
  for (int y=0; y<ycnt; y++) {
    for (int x=0; x<xcnt; x++) {
      int i = y*xcnt + x;
      
      tiles_freq[i] = 1;
      tiles_dynspr[i] = DynamicSprite.CreateFromDrawingSurface(surf, x, y, tile_width, tile_height);
    
    }
  }
}


void _AddFlippedImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], int tile_idx, eFlipDirection flip_dir)
{
  for (int i=0; i<tile_idx; i++) {
    int src_idx = i;
    int dst_idx = tile_idx + i;
    
    tiles_freq[dst_idx] = 1;
    DynamicSprite* spr_src = tiles_dynspr[src_idx];
    tiles_dynspr[dst_idx] = DynamicSprite.CreateFromExistingSprite(spr_src.Graphic, true);
    DynamicSprite* spr_dst = tiles_dynspr[dst_idx];
    spr_dst.Flip(flip_dir);    
  }
}


void _AddRotatedImages(DynamicSprite* tiles_dynspr[], int tiles_freq[], int tile_idx)
{
  
  for (int i=0; i<tile_idx; i++) {
    int src_idx = i;
    DynamicSprite* src_spr = tiles_dynspr[src_idx];
    
    for (int j=0; j<3; j++) {
      
      int dst_idx = tile_idx + i*3 + j;
      tiles_freq[dst_idx] = 1;
      
      tiles_dynspr[dst_idx] = DynamicSprite.CreateFromExistingSprite(src_spr.Graphic, true);
      DynamicSprite* spr_dst = tiles_dynspr[dst_idx];
      
      if(j != 0) {
        spr_dst.Rotate(j*90);
      }
    }
  }
}

protected void Wfc::_SwapTiles(int a_idx, int b_idx)
{
  int tmp_freq = this.TilesFrequency[a_idx];
  DynamicSprite* tmp_spr = this.TilesSprite[a_idx];
  
  this.TilesFrequency[a_idx] = this.TilesFrequency[b_idx];
  this.TilesSprite[a_idx] = this.TilesSprite[b_idx];
  
  this.TilesFrequency[b_idx] = tmp_freq;
  this.TilesSprite[b_idx] = tmp_spr;
}

protected void Wfc::_RemoveDuplicatedTiles()
{
  int unique_cnt = 1;
  for (int j=1; j<this.TilesCount; j++) {
    bool unique = true;
    for (int k=0; k<unique_cnt; k++) {
      if(_SprEqual(this.TilesSprite[j], this.TilesSprite[k])) {
        unique = false;
        this.TilesFrequency[k]++;
        break;
      }
    }
    
    
    if (unique) {
      if (unique_cnt != j) {
        this._SwapTiles(j, unique_cnt);        
      }
      
      unique_cnt++;
    }
  }

  for(int i=unique_cnt; i<this.TilesCount; i++) {
    this.TilesFrequency[i] = 0;
    this.TilesSprite[i] = null;
  }
  
  this.TilesCount = unique_cnt;
}

#define WFC_MAX_PROP_CNT 1000

////////////////////////////////////////////////////////////////////////////////
//
// WFC: Solve (Method-independent)
//
////////////////////////////////////////////////////////////////////////////////


protected void noloopcheck Wfc::_CreateProps()
{
  int prop_max = this.CellCount * WFC_MAX_PROP_CNT;
  this.Prop = new WfcProp[prop_max];
  
  for(int i=0; i<prop_max; i++)
  {
    this.Prop[i] = new WfcProp;
  }
}

protected void noloopcheck Wfc::_CreateCells()  
{
  int cell_cnt = this.CellCount;
  this.Cell = new WfcCell[cell_cnt];
  
  for (int i=0; i<cell_cnt; i++)
  {
    WfcCell* cell = new WfcCell;
    this.Cell[i] = cell;

    if(i==0) continue;
    
    int tile_count = this.TilesCount;
    
    cell.TileCount = tile_count;
  }
}

protected void Wfc::_CreateTiles()
{
  int tile_cnt = this.TilesCount;
  this.TilesFrequency = new int[tile_cnt];
  this.TilesSprite = new DynamicSprite[tile_cnt];
  
  for (int i=0; i<this.TilesCount; i++) {
    this.TilesFrequency[i] = 0;
    this.TilesSprite[i] = null;
  }
}
  
protected void Wfc::_AddProp(int src_cell_idx, int dst_cell_idx, WfcDirection direction)
{
  WfcProp* prop = this.Prop[this.PropCount];
  
  prop.SrcCellIdx = src_cell_idx;
  prop.DstCellIdx = dst_cell_idx;
  prop.direction = direction;
  
  this.PropCount++;
}

protected void Wfc::_AddPropUp(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx - this.Output.Width;
  if(dst_cell_idx < 0) return; // can't go to upper cell,  in first line
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Up);
}

protected void Wfc::_AddPropDown(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx + this.Output.Width;
  
  if(dst_cell_idx > this.CellCount) return; // can't go to down cell,  in last line
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Down);    
}

protected void Wfc::_AddPropLeft(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx - 1;
  
  if(src_cell_idx % this.Output.Width == 0) return;
  
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Left);   
}

protected void Wfc::_AddPropRight(int src_cell_idx)
{
  int dst_cell_idx = src_cell_idx + 1;
  
  if(src_cell_idx % this.Output.Width == this.Output.Width - 1) return;
    
  this._AddProp(src_cell_idx, dst_cell_idx, eWfcDir_Right);
}

protected bool Wfc::_IsTileEnabled(int tile_idx, int cell_idx, WfcDirection direction)
{
  WfcCell* cell = this.Cell[cell_idx];
  int tile_cnt = this.TilesCount;

  for(int i=0, cnt=cell.TileCount; i<cnt; i++)
  {
    if(this.allowed_tiles[_DirToIdx(direction)*this.TilesCount+tile_idx]) return true;
  }
  
  return false;
}

// Checks whether particular prop is already added and pending, in which
// case there is no point of adding the same prop again.
protected bool Wfc::_IsPropPending(int cell_idx, WfcDirection direction)
{
  for (int i=this.PropIdx+1; i < this.PropCount; i++) {
    WfcProp* prop = this.Prop[i];
    
    if(prop.SrcCellIdx == cell_idx && prop.direction == direction) return true;    
  }
  
  return false;
}


// Updates tiles in the destination cell to those that are allowed by the source cell
// and propagate updates
protected bool Wfc::_PropagateProp(WfcProp* p)
{
  int new_cnt = 0;
  
  WfcCell* dst_cell = this.Cell[p.DstCellIdx];
  
  // Go through all destination tiles and check whether they are enabled by the source cell
  for (int i=0, cnt=dst_cell.TileCount; i<cnt; i++) {
    int possible_dst_tile_idx = dst_cell.Tile[i];
    
    // If a destination tile is enabled by the source cell, keep it
    if(this._IsTileEnabled(possible_dst_tile_idx, p.SrcCellIdx, p.direction)) {
      dst_cell.Tile[new_cnt] = possible_dst_tile_idx;
      new_cnt++;
    } else {
      int freq = this.TilesFrequency[possible_dst_tile_idx];
      float p_f = IntToFloat(freq) / IntToFloat(this.FrequencySum);
      
      dst_cell.Entropy += p_f*Maths.Log(p_f);
      dst_cell.FrequencySum -= freq;
      
      if(dst_cell.FrequencySum == 0) return false;
    }
  }
  
  if(new_cnt == 0) return false;
  
  if(dst_cell.TileCount != new_cnt) {
    int dst_cell_i = p.DstCellIdx;
    if(new_cnt == 1) this.CollapsedCellCount++;
    if(p.direction != eWfcDir_Down && this._IsPropPending(dst_cell_i, eWfcDir_Up)) this._AddPropUp(dst_cell_i);
    if(p.direction != eWfcDir_Up && this._IsPropPending(dst_cell_i, eWfcDir_Down)) this._AddPropDown(dst_cell_i);
    if(p.direction != eWfcDir_Right && this._IsPropPending(dst_cell_i, eWfcDir_Left)) this._AddPropLeft(dst_cell_i);
    if(p.direction != eWfcDir_Left && this._IsPropPending(dst_cell_i, eWfcDir_Right)) this._AddPropRight(dst_cell_i);    
  }
  
  dst_cell.TileCount = new_cnt;
  
  return true;
}

protected bool Wfc::_Propagate(int cell_idx)
{
  this.PropCount = 0;
  
  this._AddPropUp(cell_idx);
  this._AddPropDown(cell_idx);
  this._AddPropLeft(cell_idx);
  this._AddPropRight(cell_idx);
  
  for(int i=0; i<this.PropCount; i++)
  {
    this.PropIdx = i;
    WfcProp* p = this.Prop[i];
    
    if(!this._PropagateProp(p)) {
      return false;
    }
  }
  
  return true;
}


protected bool Wfc::_Collapse(int cell_idx)
{
  WfcCell* cell = this.Cell[cell_idx];
  
  int remaining = Random(cell.FrequencySum);
  
  for(int i=0; i<cell.TileCount; i++) {
    int tile_idx = cell.Tile[i];
    int freq = this.TilesFrequency[tile_idx];
    
    if(remaining >= freq) {
      remaining -= freq;
    } else {
      cell.Tile[0] = cell.Tile[i];
      cell.TileCount = 1;
      cell.FrequencySum = 0;
      cell.Entropy = 0.0;
      this.CollapsedCellCount++;
      return true;
    }
  }
  
  return false;  
}

protected int Wfc::_NextCell()
{
  int min_idx = -1;
  float min_entropy = 3402823466.0;
  
  for (int i=0; i<this.CellCount; i++) {    
    WfcCell* cell = this.Cell[i];
    
    // Add small noise to break ties between tiles with the same entropy
    float entropy = cell.Entropy + (IntToFloat(Random(4096))/8388608.0);
    
    
    if (cell.TileCount != 1 && entropy < min_entropy) {
      min_entropy = entropy;
      min_idx = i;
    }
  }
  
  return min_idx;
}

protected void noloopcheck Wfc::_InitCells()
{
  int sum_freqs = 0;
  
  for(int i=0; i<this.TilesCount; i++) {
    sum_freqs += this.TilesFrequency[i];
  }
  
  this.FrequencySum = sum_freqs;
  
  float sum_plogp = 0.0;
  for (int i=0; i<this.TilesCount; i++) {
    float p_f = IntToFloat(this.TilesFrequency[i])/IntToFloat(sum_freqs);
    sum_plogp += p_f*Maths.Log(p_f);
  }
  
  float entropy = -sum_plogp;
  
  for(int i=0; i<this.CellCount; i++)
  {
    WfcCell* cell = this.Cell[i];
    
    cell.TileCount = this.TilesCount;
    cell.FrequencySum = sum_freqs;
    cell.Entropy = entropy;
    
    for(int j=0; j<this.TilesCount; j++) {
      cell.Tile[j] = j;
    }    
  }
  
  this.PropCount = 0;
}

protected void noloopcheck Wfc::_ComputeAllowedTiles()
{
  int tile_cnt = this.TilesCount;
  
  for (int d=0; d<4; d++) {
    for (int i=0; i<tile_cnt; i++) {
      for (int j=0; j<tile_cnt; j++) {
        int idx = d*i*tile_cnt+j;
        
        this.allowed_tiles[idx] = _SprCmpOverlap(this.TilesSprite[i], this.TilesSprite[j], _IdxToDir(d));
      }
    }
  }
}

protected void Wfc::_CreateTilesOverlapping(int tile_width, int tile_height, bool expand_image, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles)
{
  int xcnt = this.Input.Width - tile_width + 1;
  int ycnt = this.Input.Height - tile_height + 1;
  
  if(expand_image) {
    xcnt = this.Input.Width;
    ycnt = this.Input.Height;
    int w = this.Input.Width;
    int h = this.Input.Height;
    this.Input.Resize(w + tile_width - 1, h + tile_height -1);
  }
  
  int tile_cnt = xcnt*ycnt;
  
  if(xflip_tiles) tile_cnt *= 2;    
  if(!(xflip_tiles && rotate_tiles) && yflip_tiles) tile_cnt *= 2;
  if(rotate_tiles) tile_cnt *= 4;
  
  this.TilesCount = tile_cnt;
  this._CreateTiles();
  
  _AddOverlappingImages(this.TilesSprite, this.TilesFrequency, this.Input, xcnt, ycnt, tile_width, tile_height);
  
  int base_tile_cnt = xcnt * ycnt;
  
  if(xflip_tiles) {
    _AddFlippedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt, eFlipLeftToRight);
    base_tile_cnt *= 2;
  }
  
  if(!(xflip_tiles && rotate_tiles) && yflip_tiles) {
    _AddFlippedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt, eFlipUpsideDown);
    base_tile_cnt *= 2;
  }
  
  if(rotate_tiles) {
    _AddRotatedImages(this.TilesSprite, this.TilesFrequency, base_tile_cnt);
    base_tile_cnt *= 4;
  }
  
  this._RemoveDuplicatedTiles();
}

void Wfc::Restart()
{
  this.CollapsedCellCount = 0;
  this.Generated = false;
  this._InitCells();
}

void Wfc::Init(DynamicSprite* output, int input_sprite, int tile_width, int tile_height, bool xflip_tiles, bool yflip_tiles, bool rotate_tiles, bool expand_input)
{
  this.Output = output;
  this.Input = DynamicSprite.CreateFromExistingSprite(input_sprite, true);
  this.method = eWfc_Overlapping;
  this.CellCount = this.Output.Width * this.Output.Height;
  this.TileWidth = tile_width;
  this.TileHeight = tile_height;
  this.TilesCount = 0;
  this.FlipTilesX = xflip_tiles;
  this.FlipTilesY = yflip_tiles;
  this.RotateTiles = rotate_tiles;
  
  this._CreateTilesOverlapping(tile_width, tile_height, expand_input, xflip_tiles, yflip_tiles, rotate_tiles);
  
  this._ComputeAllowedTiles();
  
  this._CreateCells();
  this._CreateProps();
    
  this.Restart();
}

void Wfc::RegenerateOutput()
{
  DynamicSprite* out = _GenerateOutput(this.Output.Width, this.Output.Height, this.Cell, this.TilesSprite);
  
  DrawingSurface* surf = this.Output.GetDrawingSurface();
  
  surf.Clear();
  surf.DrawImage(0, 0, out.Graphic);
  surf.Release();
}

void Wfc::Run(int max_collapse_cnt)
{
  int cell_idx = Random(this.Output.Height * this.Output.Width);
  this._Rp_CellIdx = cell_idx;
  this._Rp_MaxCollapseCnt = max_collapse_cnt;
}

void Wfc::RepeatedlyUpdate(int times)
{
  for(int i=0; i<times; i++) {
    int cell_idx = this._Rp_CellIdx;
    int max_collapse_cnt = this._Rp_MaxCollapseCnt;
    
    if(cell_idx == -1 || this.CollapsedCellCount == max_collapse_cnt) {
      this.Generated = true;
      return;
    }
    
    if(!this._Collapse(cell_idx)) return;  
    if(!this._Propagate(cell_idx)) return;
    
    cell_idx = this._NextCell();
    this._Rp_CellIdx = cell_idx;
    
    if(cell_idx == -1 || this.CollapsedCellCount == max_collapse_cnt) {
      this.Generated = true;
      return;
    }
  }
}

Usage

Code: ags
Wfc wfc;
Overlay* ovr;

function room_RepExec()
{
  wfc.RepeatedlyUpdate(8192);
  
  if(wfc.Generated) {
    if(ovr == null) {
      wfc.RegenerateOutput();
      ovr = Overlay.CreateGraphical(32, 32, wfc.Output.Graphic, true);
    }
  }
}

function room_Load()
{
  wfc.Init(DynamicSprite.Create(48, 48, true), 1, 3, 3, true, true, true, false);
  wfc.Run(2048);
  
}

function room_AfterFadeIn()
{

}
#16
Don't Give Up the Cat


Tonight, you are out in the woods. As a cat.

A short game intended to be played at night, using headphones. In this first-person game you use your mouse to look around and scratch things, and WASD keys for walking, shift to run, and you use Esc to show the menu.
After you run you may sit around to breathe a bit and listen to the forest.


Game by eri0o
Music by Jon Paul Sapsford
Ghosts, graves and icon by Haloa
Gameplay Testing by Morgan Willcock, Heltenjon and Newwaveburritos

AGS Script Modules
Tween by Edmundito
Timer and Typed Text by  Crimson Wizard

Additional assets from itch io, opengameart and freesound
Forest trees, stones, flowers and base palette: Trixie (trixelized) - STRINGSTAR FIELDS
Clouds: ansimuz - Sunnyland,  recolored
House: Lanthanum - 3D Model House (PicoCAD),  recolored
Save cat: kotnaszynce - kitka.gif,  recolored+resized
Smoke: KnoblePersona - Smoke & Fire Animated Particle
Title Screen Cat: Ben - Camp Cat (PicoCAD), modified
Forest crickets and atmosphere: LokiF - Swamp Environment Audio
Cat Footsteps: melle_teich - Animal footsteps on dry leaves
Cat Jump: FOX2814 - Cat jump
Game Font: Fibberish by  Nathan Scott

This is a MAGS entry for the theme "Ghost", see original submission here.

The OST
#17


A friend made me this. Twice. I am thinking about what to do with the extra one I got! It's 3D printed and coated with some paint, pixel blue cup and matching keychain-ags tea bag - it has the pixel blue cup from the manual on the other side of the keychain too!
#18


Hey, I am starting to play around with Sprite Stacking in AGS. Has someone ever done this here?

Sliced.ash
Code: ags
// new module header
#define MAX_SLICED_OBJECTS 32
#define MAX_OVERLAYS 2048
#define OVERLAYS_STEP 32

managed struct SlicedObject {
  import void Set(float x, float y, float z, int graphic_first_slice, int graphic_last_slice);
  import static SlicedObject* Create(float x, float y, float z, int graphic_first_slice, int graphic_last_slice); // $AUTOCOMPLETEIGNORESTATIC$
  writeprotected int GraphicFirstSlice, GraphicLastSlice, SliceRange, SliceWidth, SliceHeight;
  writeprotected float X, Y, Z;
};

struct SlicedWorld{
  import void AddSlicedObject(int x, int y, int z, int graphic_first_slice, int graphic_last_slice);
  import void Render(float angle_radians = 0.0);
  protected SlicedObject* SlicedObjects[MAX_SLICED_OBJECTS];
  protected int ObjectCount;
  protected Overlay* Overlays[MAX_OVERLAYS];
};

Sliced.asc
Code: ags
// new module script
// -- SLICED OBJECT --

void SlicedObject::Set(float x, float y, float z, int graphic_first_slice, int graphic_last_slice)
{
  this.X = x;
  this.Y = y;
  this.Z = z;
  this.GraphicFirstSlice = graphic_first_slice;
  this.GraphicLastSlice = graphic_last_slice;
  this.SliceRange = graphic_last_slice - graphic_first_slice;
  this.SliceWidth = Game.SpriteWidth[graphic_first_slice];
  this.SliceHeight = Game.SpriteHeight[graphic_first_slice];
}

static SlicedObject* SlicedObject::Create(float x, float y, float z, int graphic_first_slice, int graphic_last_slice)
{
  SlicedObject* sobj = new SlicedObject;
  sobj.Set(x, y, z, graphic_first_slice, graphic_last_slice);
  return sobj;
}

// -- SLICED WORLD --

void SlicedWorld::AddSlicedObject(int x, int y, int z, int graphic_first_slice, int graphic_last_slice)
{
  SlicedObject* sobj = SlicedObject.Create(IntToFloat(x), IntToFloat(y), IntToFloat(z), graphic_first_slice, graphic_last_slice);
  
  this.SlicedObjects[this.ObjectCount] = sobj;
  this.ObjectCount++;
}


void SlicedWorld::Render(float angle_radians)
{
  float pre_cos = Maths.Cos(angle_radians);
    float pre_sin = - Maths.Sin(angle_radians);
  
  for(int i=0; i<this.ObjectCount; i++)
  {
    SlicedObject* sobj = this.SlicedObjects[i];
    
    int overlay_id_start = i*OVERLAYS_STEP;
    int slice_range = sobj.SliceRange;
    
    for(int j=0; j<slice_range; j++) {
      int overlay_id = overlay_id_start + j;
      if(this.Overlays[overlay_id] == null || !this.Overlays[overlay_id].Valid) {
        this.Overlays[overlay_id] = Overlay.CreateRoomGraphical(0, 0, sobj.GraphicFirstSlice + j);
      }
      
      Overlay* ovr = this.Overlays[overlay_id];
      
      float dist = IntToFloat(j) + sobj.Z;
      
      float lx = dist * pre_cos;
      float ly = dist * pre_sin;
      
      ovr.X = FloatToInt(sobj.X - lx);
      ovr.Y = FloatToInt(sobj.Y - ly);
      ovr.Rotation = Maths.RadiansToDegrees(angle_radians);
    }
  }
}

Usage code

Code: ags
#define SPR_BARREL 1
#define SPR_BARREL_LAST 8
#define SPR_CHAIR 9
#define SPR_CHAIR_LAST 19
#define SPR_CHEST 20
#define SPR_CHEST_LAST 30

SlicedWorld sWorld;

function room_AfterFadeIn()
{

}

function room_Load()
{
  sWorld.AddSlicedObject(32, 32, 0, SPR_BARREL, SPR_BARREL_LAST);
  sWorld.AddSlicedObject(64, 32, 0, SPR_BARREL, SPR_BARREL_LAST);
  sWorld.AddSlicedObject(64, 96, 0, SPR_CHAIR, SPR_CHAIR_LAST);
  sWorld.AddSlicedObject(128, 96, 0, SPR_CHAIR, SPR_CHAIR_LAST);
  sWorld.AddSlicedObject(200, 48, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(230, 48, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(160, 96, 0, SPR_CHEST, SPR_CHEST_LAST);
  sWorld.AddSlicedObject(32, 111, 0, SPR_CHEST, SPR_CHEST_LAST);
}

float angl;
function room_RepExec()
{
  angl += 0.0125;
  sWorld.Render(angl);
}

So far my code and usage is like so, but I am not sure on this design. Any ideas?

(link to above project zip file to download)
#19
DistFX version 0.2.0

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

AGS Script Module for Distortion Effects, based on Earthbound Battle Backgrounds.



Play with the demo!

Usage

In a room script, link before fade in and repeatedly execute, and try the example below.

Code: ags
DistFX fx; // somewhere with the same lifetime as the surface owner while distorted
Overlay* ovr;
DynamicSprite* spr;

function room_RepExec()
{
  fx.Update(Room.GetDrawingSurfaceForBackground(), spr.GetDrawingSurface(), 2 /* effect */);
  ovr.Graphic = spr.Graphic;
}

function room_Load()
{
  if(ovr == null) {
    spr = DynamicSprite.CreateFromBackground();
    ovr = Overlay.CreateGraphical(0, 0, spr.Graphic, true);
  }
}

Original Earthbound effects used a per pixel approach, but due to how AGS Script drawing performs and works, this module uses a tile based approach.

Script API

DistFX.Update
Code: ags
void DistFX.Update(DrawingSurface* source, DrawingSurface* dest, int effect);
Draws from a source surface to a destination surface using a distortion effect, from the effect bank. Currently, the available effects range is 1-135. Effect 0 appears as no effect but still goes through all the effect pipeline - and will use CPU resources the same.

DistFX.Reset
Code: ags
void DistFX.Reset();
Reset internal state, use on state change.

DistFX.DrawingTransparency
Code: ags
attribute int DistFX.DrawingTransparency;
Drawing Transparency, use for blurring the effects. Default is 0, range from 0 to 99.

DistFX.TileWidth
Code: ags
attribute int DistFX.TileWidth;
Distortion Tile Width, factor of source width, bigger is less resource intensive. Default is 64 pixels.

DistFX.TileHeight
Code: ags
attribute int DistFX.TileHeight;
Distortion Tile Height, factor of source height, bigger is less resource intensive. Default is 1 pixel.

License

This code is licensed with MIT LICENSE.
#20
Experimental Editor with AGS with Gamepad support!

Download Editor Here: >>AGS-3.99.106.0-Alpha3-JoystickPR.zip<< | installer | Code Source | GitHub Issue | GitHub PR

First the concepts, joystick means a generic input device comprised by binary (button) and analog (axis) inputs. A Gamepad means something that is vaguely close to a Xbox360 controller.

We can expand the API later, but the basics is joystick connection and disconnection is handled inside the AGS Engine and we can skip things by pressing A,B,X,Y or the symbols in PS controller.



@Alan v.Drake made a beautiful test project!



Playable AgsGameGamepadV4.zip | project | Online https://ericoporto.github.io/agsjs/gamepadtest/




Joystick Static Methods

Joystick.JoystickCount
Code: ags
static readonly int Joystick.JoystickCount
Get the number of connected joysticks. No joysticks should return 0!


Joystick.Joysticks
Code: ags
static readonly Joystick* Joystick.Joysticks[int index]
Gets a joystick by index from the internal engine joystick list.


Joystick Instance Attributes and Methods

Joystick.IsConnected
Code: ags
readonly bool Joystick.IsConnected
True if joystick is connected.


Joystick.Name
Code: ags
readonly String Joystick.Name
joystick name.



Joystick.IsButtonDown
Code: ags
bool Joystick.IsButtonDown(int button)
checks if a joystick button is pressed, by index. DPad is usually mapped separately as a hat.


Joystick.GetAxis
Code: ags
bool Joystick.GetAxis(int axis, optional float deadzone)
get a joystick axis or trigger, trigger only has positive values, by axis number. Values varies from -1.0 to 1.0 for axis, and 0.0 to 1.0 for triggers.


Joystick.GetHat
Code: ags
eJoystick_Hat Joystick.GetHat(int hat)
returns hat value


Joystick.AxisCount
Code: ags
readonly int Joystick.AxisCount
get the number of axis in the joystick


Joystick.ButtonCount
Code: ags
readonly int Joystick.ButtonCount
get the number of buttons in the joystick


Joystick.HatCount
Code: ags
readonly int Joystick.HatCount
get the number of hats in the joystick



Joystick.IsGamepad
Code: ags
readonly bool Joystick.IsGamepad
True if joystick is a valid gamepad connected - this means SDL2 recognized it as a valid GameController and has successfully mapped it's buttons to an Xbox360 gamepad.


Joystick.IsGamepadButtonDown
Code: ags
bool Joystick.IsGamepadButtonDown(eGamepad_Button button)
checks if a gamepad button is pressed, including dpad.

Possible buttons:
  • eGamepad_ButtonA
  • eGamepad_ButtonB
  • eGamepad_ButtonX
  • eGamepad_ButtonY
  • eGamepad_ButtonBack
  • eGamepad_ButtonGuide
  • eGamepad_ButtonStart
  • eGamepad_ButtonLeftStick
  • eGamepad_ButtonRightStick
  • eGamepad_ButtonLeftShoulder
  • eGamepad_ButtonRightShoulder
  • eGamepad_ButtonDpadUp
  • eGamepad_ButtonDpadDown
  • eGamepad_ButtonDpadLeft
  • eGamepad_ButtonDpadRight


Joystick.GetGamepadAxis
Code: ags
float Joystick.GetGamepadAxis(eGamepad_Axis axis, optional float deadzone)
get gamepad axis or trigger, trigger only has positive values. Values varies from -1.0 to 1.0 for axis, and 0.0 to 1.0 for triggers.

You can optionally pass an additional parameter to use as deadzone. If an axis absolute value is smaller than the value of deadzone, it will return 0.0. Default value is AXIS_DEFAULT_DEADZONE, which is for now 0.125, use the name if you need a number.

Possible axis and triggers:
  • eGamepad_AxisLeftX
  • eGamepad_AxisLeftY
  • eGamepad_AxisRightX
  • eGamepad_AxisRightY
  • eGamepad_AxisTriggerLeft
  • eGamepad_AxisTriggerRight


CHANGELOG:
  • v1: initial release
  • v2: GetAxis now returns a float
  • v3: GetAxis now has an additional dead_zone parameter. Default value is GAMEPAD_DEFAULT_DEADZONE, which is for now 0.125, use the name if you need a number.
  • v4: Using correspondent A,B,X,Y buttons can skip Speech and Display messages, as long as you Connect to the Gamepad.
  • v5: ditched Gamepad-only approach for a Joystick first approach similar to löve.
  • v6: connection and disconnection handled in-engine, new AGS 4 approach.
  • v7: api renamed get,getcount to instead retrieve from array of joysticks, also fixed save/load game bug
SMF spam blocked by CleanTalk