Hello, I'm making a Top-Down Space Shooter game in...
Tạo vào: 9 tháng 1, 2026
Tạo vào: 9 tháng 1, 2026
Hello, I'm making a Top-Down Space Shooter game in Game Maker Studio,
I will need you to write code for me and suggest gameplay mechanics and ideas so I can focus on visual and sound design
use lowerCamelCase format and Allman Indentation style
naming formats:
What I already have:
Im using oEnemyM1 as a placeholder for the oEnemyL1 which should fire rocket barrage
Dont draw effect/particles or sounds, ill handle that by myself
instead of close-range lasers we will now fire close-range cannons (oCannonBullet) (2 cannons, 1 on each side, should fire one by one (L>R x3, fast reload, L>R x3))
the sprite for oEnemyL1 is texEnemyL1 and is 128x128 px, origin is set at 56 x 64
Left cannon muzzle point: 107 x 50
Right cannon muzzle point: 107 x 78
Left rocket barrage (in a diagonal line): 70 x 14 > 86 x 32
Right rocket barrage (in a diagonal line): 70 x 114 > 86 x 96
(add an adjustable outward shift for barrages to avoid colliding with own rockets)
the default facing (0 degrees) is to the right
remove any unused lines left from the M1, like lock-on or missles (we will only use rockets)
keep the code as simple as possible and optimised
the ship should:
when player is out of range > patrol
when player is in range > close in to firing range
when player is in firing range > "arm" rockets (a small delay before firing the barrage, draw a rocket alarm sprite (oEnemyL1Danger)) > fire rocket barrage > reload barrage (3 reloads max)
when player is in close range > fire cannons
also leave the flares from oEnemyM1, they work fine
oEnemyL1
Create Event:
event_inherited();
hp = real(80);
infoName = ("L1 'Hurricane'");
mass = 7000;
threatAmount = 60;
xpAmount = 150;
circleRadius = 118;
textOffset = -1280;
armor = 4;
ricochetMult = 20;
sparksChance = 12;
flareCooldown = 0;
frMin = 5;
frMax = 65;
flareReactionTime = random_range(frMin, frMax); // Random reaction
canDeployFlares = true;
hasFlares = true
flaresAmount = 12;
if hasFlares
{
flaresAmmo = 3;
}else flaresAmmo = 0;
guardRadius = 7000;
bulletSpeed = 100;
canShoot = true;
search = random_range(-400,400);
fireDist = 2600;
chaseDist = 4500;
canShootGun = true;
target = noone;
Step:
event_inherited();
playerExist = instance_exists(oPlayer);
playerInLOS = collision_line(x,y,x+lengthdir_x(fireDist, image_angle),y+lengthdir_y(fireDist, image_angle),oPlayer,false,true);
//fCollPl = collision_line(x, y, oPlayer.x, oPlayer.y, grpHostiles, false, true);
#region Speed Settings
// Set Max Speed
speed = clamp(speed, 0, 8);
if(speed > 0)
{
// Slowdown
speed -= 0.25;
}
if speed = 0
{
sprite_index = texEnemyM1;
}
// Get Local Speed & Rotation
var localSpeed = speed;
var localAngle = image_angle;
#endregion Speed Settings
#region Swarm Coordination
nearest = instance_nearest(x,y,grpHostiles);
if distance_to_object(grpHostiles) < 200
{
var _inst = instance_place(x, y, grpHostiles);
if (_inst != noone) {
var _diff_x = x - _inst.x;
var _diff_y = y - _inst.y;
hspeed = hspeed + (_diff_x / 64);
vspeed = vspeed + (_diff_y / 64);
}
} #endregion Swarm Coordination
#region Check Player & Distance
if global.playerExists
{
{
target = oPlayer;
distToTarget = point_distance(x,y,target.x,target.y);
var Time = distToTarget*1.5 / bulletSpeed;
lead_x = target.x + (target.hspeed - hspeed) *Time;
lead_y = target.y + (target.vspeed - vspeed) *Time;
}
if playerDist<chaseDist
{ // Look at Player with max turn speed
var lookDir=point_direction(x,y,lead_x,lead_y)
var rotFactor=-4.5 // Turn Speed
image_angle+=sin(degtorad(image_angle-lookDir))*rotFactor;
}
// Check Distance & Move to Player
if (playerDist>fireDist-10) && distance_to_object(oRefuelPad)>3000
{ // Add Motion
motion_add(point_direction(x,y,oPlayer.x,oPlayer.y), 1.7);
draw_sprite_ext(texEnemyM1Thrust, -1, x, y, 1, 1, image_angle, -1, 1);
image_speed = 1
} else sprite_index = texEnemyM1
if distance_to_object(oRefuelPad)<3000
{
motion_add(point_direction(oRefuelPad.x,oRefuelPad.y,x,y), 1.7);
draw_sprite_ext(texEnemyM1Thrust, -1, x, y, 1, 1, image_angle, -1, 1);
}
}else target = noone #endregion Check Player & Distance
if playerExist
{
checkForFriendly = !collision_line(x, y, oPlayer.x, oPlayer.y, grpHostiles, true, 1)
// && !collision_ellipse(x + -32, y + -32, x + 32, y + 32, grpHostiles, true, 1);
}
#region Launch Rockets
if canShoot = true && playerDist<fireDist && playerDist>1200 && checkForFriendly && !global.playerSafe && !oPlayer.trailActive
{
enemyRocket1 = (instance_create_layer
(x + lengthdir_x(60, image_angle), y + lengthdir_y(60, image_angle), "Instances", oRocket1));
with(enemyRocket1)
{
direction = other.image_angle;
image_angle = other.image_angle;
speed = +other.speed;
hostile = true;
}
canShoot = false;
alarm_set (0, 800); // Firerate
} #endregion Launch Missile
if playerExist // Laser
{
if canShootGun && playerDist<fireDist-900 && checkForFriendly && playerInLOS && !global.playerSafe && !oPlayer.trailActive
{
enemyLaser = (instance_create_layer(x + lengthdir_x(12, image_angle), y + lengthdir_y(12, image_angle), "Instances", oLaser));
with(enemyLaser)
{
hostile = true
//image_xscale = 2
speed = 45
direction = localAngle+random_range(-1,1)
image_angle = direction
hspeed += other.hspeed
vspeed += other.vspeed
}
part_particles_burst(global.partSystem, x + lengthdir_x(40, image_angle), y + lengthdir_y(40, image_angle), psMissileFire);
canShootGun = false;
alarm_set (2, 8); // Firerate Burst
}
}
#region Flare Deployment
// Check if we should deploy flares
if (hasFlares && flaresAmmo > 0 && canDeployFlares)
{
var shouldDeployFlares = false;
text// Check for incoming missiles targeting this ship with (oMissilePhase2) { var distToMissile = point_distance(other.x, other.y, x, y); if (distToMissile < 900) // Missile is close enough to trigger flare deployment { shouldDeployFlares = true; break; } } // Random chance based on reaction time if (shouldDeployFlares) { flareCooldown++; if (flareCooldown >= flareReactionTime) { DeployFlares(); flareCooldown = 0; flareReactionTime = random_range(frMin, frMax); // Reset with new random reaction time } } else { // Reset cooldown if no immediate threat flareCooldown = 0; }
}
#endregion Flare Deployment
#region LowHP
if hp < 25
{
sprite_index =
texEnemyM1Damaged;
image_speed = 0;
image_index = 0;
var xRange = random_range(-16, 16)
var yRange = random_range(-16, 16)
if irandom(sparksChance) > sparksChance-1
{
part_particles_burst(global.partSystem, x + xRange, y + yRange, psSparksLowHP);
}
if irandom(sparksChance) > sparksChance-1
{
part_particles_burst(global.partSystem, x + xRange, y + yRange, psFireBurst);
}
}
if hp < 15
{
sparksChance = 1;
}
#endregion LowHP
#region If Health 0
if(hp <= 0)
{
// Create sfxExplosion
part_particles_burst(global.partSystem, x, y, psExplosion1);
part_particles_burst(global.partSystem, x, y, psShipExplodeSmall);
part_particles_burst(global.partSystem, x, y, psShipExplode);
// Play Audio
oSoundManager.playSoundAt(sfxExplosion, x, y, global.sndRocketExplodeDist,
global.sndRocketExplodeDist*1.5, false, 2, global.sndRocketExplodeGain, random_range(0.8, 1.2));
if instance_exists(oEnemyLockOn) && oEnemyLockOn.parentShip = self.id
{
instance_destroy(myLock);
}
wreck = instance_create_layer(x,y, "Instances", oEnemyM1Destroyed); // Destroy Self
with (wreck)
{
image_angle = other.image_angle;
direction = other.direction;
vspeed = other.vspeed;
hspeed = other.hspeed;
}
instance_destroy();
}
#endregion If Health 0
Alarm 0: canShoot = true;
Alarm 2: canShootGun = true;
Alarm 11: // Flare Cooldown
canDeployFlares = true;
Below is a clean oEnemyL1 implementation that replaces the old M1 missile/lock-on/laser logic with:
No particles, no draw calls, no sounds.
All code uses lowerCamelCase and Allman indentation.
Each event is separate.
gmlevent_inherited(); // Core Stats hp = real(80); infoName = "L1 'Hurricane'"; mass = 7000; threatAmount = 60; xpAmount = 150; circleRadius = 118; textOffset = -1280; armor = 4; ricochetMult = 20; // Behavior Distances guardRadius = 7000; chaseDist = 4500; fireDist = 2600; closeDist = 1200; // Movement search = random_range(-400, 400); // Targeting target = noone; targetDist = 999999; playerInLos = false; checkForFriendly = true; // Speed Settings maxSpeed = 8; slowdown = 0.25; // --- Rocket Barrage Settings --- rocketArmTime = 40; // delay before firing after "arming" rocketReloadTime = 120; // short reload between barrages rocketMagazineReloadTime = 420; // long reload after 3 barrages barrageShotsMax = 3; barrageShotsLeft = barrageShotsMax; rocketsPerSide = 6; // rockets along each diagonal line rocketSpeed = 18; rocketSpread = 2; // degrees random rocketOutShift = 10; // adjustable outward shift to avoid self-collision rocketState = 0; // 0=ready, 1=arming, 2=reloading, 3=magReload rocketTimer = 0; rocketCanFire = true; dangerInst = noone; // --- Cannon Settings --- cannonRange = closeDist; cannonShotDelay = 4; // fast L/R spacing cannonBurstReload = 28; // reload after L>R x3 cannonTimer = 0; cannonIndex = 0; // 0..5 (L,R,L,R,L,R) cannonCanFire = true; // --- Flares (kept) --- flareCooldown = 0; frMin = 5; frMax = 65; flareReactionTime = random_range(frMin, frMax); canDeployFlares = true; hasFlares = true; flaresAmount = 12; if (hasFlares) { flaresAmmo = 3; } else { flaresAmmo = 0; } // Sprite sprite_index = texEnemyL1; image_speed = 0; // --- Local Helpers --- getWorldFromSprite = function(_sx, _sy) { // Convert sprite-space point (_sx,_sy) to world-space, accounting for origin + rotation var _dx = _sx - sprite_get_xoffset(texEnemyL1); var _dy = _sy - sprite_get_yoffset(texEnemyL1); var _wx = x + lengthdir_x(_dx, image_angle) + lengthdir_x(_dy, image_angle + 90); var _wy = y + lengthdir_y(_dx, image_angle) + lengthdir_y(_dy, image_angle + 90); return [ _wx, _wy ]; }; spawnRocket = function(_wx, _wy, _dir) { var _r = instance_create_layer(_wx, _wy, "Instances", oRocket1); with (_r) { hostile = true; direction = _dir; image_angle = _dir; speed = other.rocketSpeed; hspeed += other.hspeed; vspeed += other.vspeed; } return _r; }; fireBarrageSide = function(_x1, _y1, _x2, _y2, _sideSign) { // _sideSign: -1 for "left/top" (shift outward one way), +1 for "right/bottom" var _normalAng = image_angle + 90; for (var i = 0; i < rocketsPerSide; i++) { var t = 0; if (rocketsPerSide > 1) { t = i / (rocketsPerSide - 1); } var _sx = lerp(_x1, _x2, t); var _sy = lerp(_y1, _y2, t); // Convert to world var _p = getWorldFromSprite(_sx, _sy); var _wx = _p[0]; var _wy = _p[1]; // Outward shift _wx += lengthdir_x(rocketOutShift * _sideSign, _normalAng); _wy += lengthdir_y(rocketOutShift * _sideSign, _normalAng); // Fire var _dir = image_angle + random_range(-rocketSpread, rocketSpread); spawnRocket(_wx, _wy, _dir); } }; startArming = function() { rocketState = 1; rocketTimer = rocketArmTime; if (!instance_exists(dangerInst)) { dangerInst = instance_create_layer(x, y, "Instances", oEnemyL1Danger); with (dangerInst) { parentShip = other.id; } } }; finishArmingAndFire = function() { // Destroy danger indicator if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; // Left barrage diagonal: 70x14 -> 86x32 (top) fireBarrageSide(70, 14, 86, 32, -1); // Right barrage diagonal: 70x114 -> 86x96 (bottom) fireBarrageSide(70, 114, 86, 96, +1); barrageShotsLeft -= 1; if (barrageShotsLeft > 0) { rocketState = 2; rocketTimer = rocketReloadTime; } else { rocketState = 3; rocketTimer = rocketMagazineReloadTime; } rocketCanFire = false; alarm_set(0, 1); }; fireCannonShot = function(_muzzleSpriteX, _muzzleSpriteY) { var _p = getWorldFromSprite(_muzzleSpriteX, _muzzleSpriteY); var _b = instance_create_layer(_p[0], _p[1], "Instances", oCannonBullet); with (_b) { hostile = true; direction = other.image_angle; image_angle = direction; speed = 55; hspeed += other.hspeed; vspeed += other.vspeed; } };
gmlevent_inherited(); // Clamp & Slowdown speed = clamp(speed, 0, maxSpeed); if (speed > 0) { speed -= slowdown; if (speed < 0) { speed = 0; } } sprite_index = texEnemyL1; image_speed = 0; // Swarm Separation (lightweight) if (distance_to_object(grpHostiles) < 200) { var inst = instance_place(x, y, grpHostiles); if (inst != noone) { var diffX = x - inst.x; var diffY = y - inst.y; hspeed += (diffX / 64); vspeed += (diffY / 64); } } // Target Acquire if (instance_exists(oPlayer) && global.playerExists) { target = oPlayer; targetDist = point_distance(x, y, target.x, target.y); // Friendly fire check (simple) checkForFriendly = !collision_line(x, y, target.x, target.y, grpHostiles, true, true); // LOS (simple forward probe) playerInLos = collision_line( x, y, x + lengthdir_x(fireDist, image_angle), y + lengthdir_y(fireDist, image_angle), oPlayer, false, true ); } else { target = noone; targetDist = 999999; playerInLos = false; checkForFriendly = true; } // Rotation + Steering if (target != noone) { // Lead a bit (simple) var timeToTarget = (targetDist * 1.5) / max(1, rocketSpeed); var leadX = target.x + (target.hspeed - hspeed) * timeToTarget; var leadY = target.y + (target.vspeed - vspeed) * timeToTarget; // Turn speed var lookDir = point_direction(x, y, leadX, leadY); var rotFactor = -4.5; image_angle += sin(degtorad(image_angle - lookDir)) * rotFactor; } else { // Patrol rotate slowly using search image_angle += sin(degtorad(search)) * -1.25; search += random_range(-2, 2); } // --- State Behavior --- var playerInChase = (target != noone && targetDist < chaseDist); var playerInFire = (target != noone && targetDist < fireDist); var playerInClose = (target != noone && targetDist < cannonRange); // Movement: Patrol vs Chase to firing band if (!playerInChase) { // Patrol drift motion_add(image_angle + search, 0.35); } else { // Close in until within firing range band if (targetDist > fireDist - 60) { motion_add(point_direction(x, y, target.x, target.y), 1.7); } else if (targetDist < closeDist - 120) { // Back off slightly if too close (gives space for rockets/cannons) motion_add(point_direction(target.x, target.y, x, y), 0.9); } } // --- Rocket Barrage Logic --- var rocketOk = ( target != noone && playerInFire && !playerInClose && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (rocketOk) { if (rocketState == 0 && rocketCanFire) { startArming(); } } // Run rocket timers if (rocketState == 1) { rocketTimer -= 1; if (rocketTimer <= 0) { // Only fire if still valid if (rocketOk) { finishArmingAndFire(); } else { // Cancel arming rocketState = 0; rocketTimer = 0; if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; } } } else if (rocketState == 2) { rocketTimer -= 1; if (rocketTimer <= 0) { rocketState = 0; rocketTimer = 0; } } else if (rocketState == 3) { rocketTimer -= 1; if (rocketTimer <= 0) { barrageShotsLeft = barrageShotsMax; rocketState = 0; rocketTimer = 0; } } // --- Cannon Logic (Close Range) --- var cannonOk = ( target != noone && playerInClose && checkForFriendly && playerInLos && !global.playerSafe && !oPlayer.trailActive ); // Don’t fire cannons while arming rockets (keeps readability/telegraphing) if (cannonOk && cannonCanFire && rocketState != 1) { if (cannonTimer <= 0) { // Alternate L/R, 3 cycles => 6 shots total if ((cannonIndex % 2) == 0) { // Left muzzle point: 107 x 50 fireCannonShot(107, 50); } else { // Right muzzle point: 107 x 78 fireCannonShot(107, 78); } cannonIndex += 1; if (cannonIndex >= 6) { cannonIndex = 0; cannonTimer = cannonBurstReload; } else { cannonTimer = cannonShotDelay; } } } if (cannonTimer > 0) { cannonTimer -= 1; } // --- Flare Deployment (kept) --- if (hasFlares && flaresAmmo > 0 && canDeployFlares) { var shouldDeployFlares = false; with (oMissilePhase2) { var distToMissile = point_distance(other.x, other.y, x, y); if (distToMissile < 900) { shouldDeployFlares = true; break; } } if (shouldDeployFlares) { flareCooldown += 1; if (flareCooldown >= flareReactionTime) { DeployFlares(); flareCooldown = 0; flareReactionTime = random_range(frMin, frMax); } } else { flareCooldown = 0; } } // Cleanup danger indicator if parent is gone (safety) if (instance_exists(dangerInst)) { if (dangerInst.parentShip != id) { instance_destroy(dangerInst); dangerInst = noone; } } // Death is handled by parent (event_inherited) or your existing system. // If you still need custom wreck creation, put it here, but you asked to keep it simple.
gml// Simple gate: re-enable rocket firing next step (prevents double-trigger same frame) rocketCanFire = true;
gml// Not used by this L1 setup, but kept to avoid breaking inherited logic if something calls it. cannonCanFire = true;
gmlcanDeployFlares = true;
If you want, I can also give you a tiny oEnemyL1Danger object (Create/Step/Draw) that simply follows parentShip and self-destructs if the ship dies — with no visuals (you’ll assign the sprite and draw settings).
It shoots the player only when directly facing him, but not when leading the target. Also make sure the rockets fire in turn, from inner to outer (symmetrical wings). Give it a more advanced AI, with strafing, avoiding collision (grpCollide is a parent for all objects with collision), patrolling
Got it — two fixes + a smarter movement brain:
grpCollide (and light separation vs grpHostiles).Below is a drop-in rewrite for oEnemyL1 Create + Step + Alarm 11 (keeps your inherited stuff + flares).
No particles/sounds/draw calls.
gmlevent_inherited(); // Core Stats hp = real(80); infoName = "L1 'Hurricane'"; mass = 7000; threatAmount = 60; xpAmount = 150; circleRadius = 118; textOffset = -1280; armor = 4; ricochetMult = 20; sparksChance = 12; // Home / Patrol homeX = x; homeY = y; guardRadius = 7000; patrolTimer = 0; patrolIntervalMin = 120; patrolIntervalMax = 260; patrolTargetX = x; patrolTargetY = y; // Distances chaseDist = 4500; fireDist = 2600; closeDist = 1200; // Movement maxSpeed = 8; slowdown = 0.25; seekAccel = 1.6; patrolAccel = 0.55; brakeAccel = 0.9; avoidLookAhead = 220; avoidStrength = 2.2; separationRadius = 200; separationStrength = 1.0; // Strafing strafeDir = choose(-1, 1); strafeTimer = irandom_range(120, 220); strafeStrength = 1.15; // Targeting target = noone; targetDist = 999999; checkForFriendly = true; // --- Rockets (Barrage) --- rocketArmTime = 40; rocketVolleyDelay = 3; // delay between each rocket in the sequence rocketReloadTime = 120; // reload between barrages rocketMagazineReloadTime = 420; // long reload after 3 barrages barrageShotsMax = 3; barrageShotsLeft = barrageShotsMax; rocketsPerSide = 6; rocketSpeed = 18; rocketSpread = 2; rocketOutShift = 10; rocketState = 0; // 0=ready, 1=arming, 2=volley, 3=reload, 4=magReload rocketTimer = 0; rocketVolleyTimer = 0; rocketVolleySide = 0; // 0=left, 1=right rocketVolleyRing = rocketsPerSide - 1; // inner->outer (inner is closer to center: y 32 / 96) dangerInst = noone; // --- Cannons (Close range) --- cannonRange = closeDist; cannonShotDelay = 4; // fast L/R spacing cannonBurstReload = 28; // after L>R x3 cannonTimer = 0; cannonIndex = 0; // 0..5 (L,R,L,R,L,R) // --- Flares (kept) --- flareCooldown = 0; frMin = 5; frMax = 65; flareReactionTime = random_range(frMin, frMax); canDeployFlares = true; hasFlares = true; flaresAmount = 12; if (hasFlares) { flaresAmmo = 3; } else { flaresAmmo = 0; } // Sprite sprite_index = texEnemyL1; image_speed = 0; // Helpers getWorldFromSprite = function(_sx, _sy) { var dx = _sx - sprite_get_xoffset(texEnemyL1); var dy = _sy - sprite_get_yoffset(texEnemyL1); var wx = x + lengthdir_x(dx, image_angle) + lengthdir_x(dy, image_angle + 90); var wy = y + lengthdir_y(dx, image_angle) + lengthdir_y(dy, image_angle + 90); return [ wx, wy ]; }; getBarragePoint = function(_side, _ringIndex) { // Left diagonal: 70,14 -> 86,32 (top) // Right diagonal: 70,114 -> 86,96 (bottom) var t = 0; if (rocketsPerSide > 1) { t = _ringIndex / (rocketsPerSide - 1); } var sx1, sy1, sx2, sy2; var sideSign; if (_side == 0) { sx1 = 70; sy1 = 14; sx2 = 86; sy2 = 32; sideSign = -1; } else { sx1 = 70; sy1 = 114; sx2 = 86; sy2 = 96; sideSign = 1; } var sx = lerp(sx1, sx2, t); var sy = lerp(sy1, sy2, t); var p = getWorldFromSprite(sx, sy); var wx = p[0]; var wy = p[1]; // outward shift var normalAng = image_angle + 90; wx += lengthdir_x(rocketOutShift * sideSign, normalAng); wy += lengthdir_y(rocketOutShift * sideSign, normalAng); return [ wx, wy ]; }; spawnRocketAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY) + random_range(-rocketSpread, rocketSpread); var r = instance_create_layer(_wx, _wy, "Instances", oRocket1); with (r) { hostile = true; direction = dir; image_angle = dir; speed = other.rocketSpeed; hspeed += other.hspeed; vspeed += other.vspeed; } }; spawnCannonAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY); var b = instance_create_layer(_wx, _wy, "Instances", oCannonBullet); with (b) { hostile = true; direction = dir; image_angle = dir; speed = 55; hspeed += other.hspeed; vspeed += other.vspeed; } }; pickNewPatrolPoint = function() { var ang = random(360); var rad = random_range(800, guardRadius); patrolTargetX = homeX + lengthdir_x(rad, ang); patrolTargetY = homeY + lengthdir_y(rad, ang); patrolTimer = irandom_range(patrolIntervalMin, patrolIntervalMax); }; startArming = function() { rocketState = 1; rocketTimer = rocketArmTime; if (!instance_exists(dangerInst)) { dangerInst = instance_create_layer(x, y, "Instances", oEnemyL1Danger); with (dangerInst) { parentShip = other.id; } } }; startVolley = function() { rocketState = 2; rocketVolleyTimer = 0; rocketVolleySide = 0; rocketVolleyRing = rocketsPerSide - 1; // inner -> outer if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; }; endBarrageAndReload = function() { barrageShotsLeft -= 1; if (barrageShotsLeft > 0) { rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketState = 4; rocketTimer = rocketMagazineReloadTime; } };
gmlevent_inherited(); sprite_index = texEnemyL1; image_speed = 0; // Speed clamp + slowdown speed = clamp(speed, 0, maxSpeed); if (speed > 0) { speed -= slowdown; if (speed < 0) { speed = 0; } } // Target acquire if (instance_exists(oPlayer) && global.playerExists) { target = oPlayer; targetDist = point_distance(x, y, target.x, target.y); // Friendly fire check (keep it simple) checkForFriendly = !collision_line(x, y, target.x, target.y, grpHostiles, true, true); } else { target = noone; targetDist = 999999; checkForFriendly = true; } // Patrol point management if (patrolTimer <= 0) { pickNewPatrolPoint(); } else { patrolTimer -= 1; } // Strafe switching strafeTimer -= 1; if (strafeTimer <= 0) { strafeDir *= -1; strafeTimer = irandom_range(120, 220); } // --- Separation from other hostiles (lightweight) --- if (distance_to_object(grpHostiles) < separationRadius) { var inst = instance_place(x, y, grpHostiles); if (inst != noone) { var diffX = x - inst.x; var diffY = y - inst.y; var len = max(1, point_distance(0, 0, diffX, diffY)); diffX /= len; diffY /= len; hspeed += diffX * separationStrength; vspeed += diffY * separationStrength; } } // --- Collision avoidance vs grpCollide --- var avoidX = 0; var avoidY = 0; var lookX = x + lengthdir_x(avoidLookAhead, image_angle); var lookY = y + lengthdir_y(avoidLookAhead, image_angle); if (collision_line(x, y, lookX, lookY, grpCollide, true, true)) { // steer sideways away from forward direction (simple, effective) avoidX += lengthdir_x(avoidStrength, image_angle + 90); avoidY += lengthdir_y(avoidStrength, image_angle + 90); } // --- Decide movement mode --- var inChase = (target != noone && targetDist < chaseDist); var inFire = (target != noone && targetDist < fireDist); var inClose = (target != noone && targetDist < closeDist); // Compute lead points (this is the key fix: aim independent of facing) var rocketAimX = 0; var rocketAimY = 0; var cannonAimX = 0; var cannonAimY = 0; if (target != noone) { // Rocket lead var tR = targetDist / max(1, rocketSpeed); rocketAimX = target.x + (target.hspeed - hspeed) * tR; rocketAimY = target.y + (target.vspeed - vspeed) * tR; // Cannon lead var cannonBulletSpeed = 55; var tC = targetDist / max(1, cannonBulletSpeed); cannonAimX = target.x + (target.hspeed - hspeed) * tC; cannonAimY = target.y + (target.vspeed - vspeed) * tC; // Rotate toward rocket lead (not required to fire anymore, but helps look smart) var lookDir = point_direction(x, y, rocketAimX, rocketAimY); var rotFactor = -4.5; image_angle += sin(degtorad(image_angle - lookDir)) * rotFactor; } else { // Gentle patrol rotation toward patrol point var pDir = point_direction(x, y, patrolTargetX, patrolTargetY); var pRot = -2.8; image_angle += sin(degtorad(image_angle - pDir)) * pRot; } // --- Movement vectors --- if (!inChase) { // Patrol: move toward patrol point, with avoidance var pMoveDir = point_direction(x, y, patrolTargetX, patrolTargetY); motion_add(pMoveDir, patrolAccel); hspeed += avoidX; vspeed += avoidY; } else { // Combat movement: approach / hold / strafe + avoidance var toTargetDir = point_direction(x, y, target.x, target.y); // Distance banding if (targetDist > fireDist - 80) { motion_add(toTargetDir, seekAccel); } else if (targetDist < closeDist - 140) { motion_add(toTargetDir + 180, brakeAccel); } // Strafe in firing zone (keeps it moving and harder to hit) if (inFire) { motion_add(toTargetDir + (90 * strafeDir), strafeStrength); } hspeed += avoidX; vspeed += avoidY; } // --- Rocket Barrage Logic (advanced + ordered volley) --- var rocketOk = ( target != noone && inFire && !inClose && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (rocketState == 0) { if (rocketOk) { startArming(); } } else if (rocketState == 1) { // arming rocketTimer -= 1; if (!rocketOk) { rocketState = 0; rocketTimer = 0; if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; } else if (rocketTimer <= 0) { startVolley(); } } else if (rocketState == 2) { // Volley: inner -> outer, alternating Left then Right if (!rocketOk) { // cancel volley if conditions break rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketVolleyTimer -= 1; if (rocketVolleyTimer <= 0) { if (rocketVolleyRing >= 0) { var p = getBarragePoint(rocketVolleySide, rocketVolleyRing); spawnRocketAt(p[0], p[1], rocketAimX, rocketAimY); // Alternate side. After Right, step outward (ring--) if (rocketVolleySide == 0) { rocketVolleySide = 1; } else { rocketVolleySide = 0; rocketVolleyRing -= 1; } rocketVolleyTimer = rocketVolleyDelay; } else { // Done firing this barrage endBarrageAndReload(); } } } } else if (rocketState == 3) { // reload between barrages rocketTimer -= 1; if (rocketTimer <= 0) { rocketState = 0; rocketTimer = 0; } } else if (rocketState == 4) { // magazine reload after 3 barrages rocketTimer -= 1; if (rocketTimer <= 0) { barrageShotsLeft = barrageShotsMax; rocketState = 0; rocketTimer = 0; } } // --- Cannons (close range, lead aim, alternating L>R x3) --- var cannonOk = ( target != noone && inClose && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); // Optional: stop cannons while arming rockets only (telegraph clarity) if (cannonOk && rocketState != 1) { if (cannonTimer <= 0) { // muzzle points on texEnemyL1 if ((cannonIndex % 2) == 0) { var pL = getWorldFromSprite(107, 50); spawnCannonAt(pL[0], pL[1], cannonAimX, cannonAimY); } else { var pR = getWorldFromSprite(107, 78); spawnCannonAt(pR[0], pR[1], cannonAimX, cannonAimY); } cannonIndex += 1; if (cannonIndex >= 6) { cannonIndex = 0; cannonTimer = cannonBurstReload; } else { cannonTimer = cannonShotDelay; } } } if (cannonTimer > 0) { cannonTimer -= 1; } // --- Flares (kept) --- if (hasFlares && flaresAmmo > 0 && canDeployFlares) { var shouldDeployFlares = false; with (oMissilePhase2) { var distToMissile = point_distance(other.x, other.y, x, y); if (distToMissile < 900) { shouldDeployFlares = true; break; } } if (shouldDeployFlares) { flareCooldown += 1; if (flareCooldown >= flareReactionTime) { DeployFlares(); flareCooldown = 0; flareReactionTime = random_range(frMin, frMax); } } else { flareCooldown = 0; } } // Safety: keep danger attached and cleaned up if (instance_exists(dangerInst)) { dangerInst.x = x; dangerInst.y = y; dangerInst.image_angle = image_angle; if (dangerInst.parentShip != id) { instance_destroy(dangerInst); dangerInst = noone; } }
gmlcanDeployFlares = true;
No more “must face player to shoot”
Rockets and cannons now compute rocketAimX/Y and cannonAimX/Y using target velocity + your velocity, then fire using point_direction(spawn, aim).
Rockets fire inner → outer, alternating sides
rocketVolleyRing starts at rocketsPerSide - 1 (inner points: y 32 / 96) and steps outward.
Sequence: Left(inner), Right(inner), Left(next), Right(next)…
Advanced AI movement
guardRadius around homeX/homeYgrpCollide and adds lateral steeringIf you want the avoidance to feel even smarter (still simple/fast), I can add a 2-ray or 3-ray probe (forward + forward-left + forward-right) to decide which side to dodge toward — it makes them stop “always dodging the same side” around asteroids.
it always strafes to left/right making a zig-zag pattern. I want it to strafe rarely. Also make sure it closes in on player to fire with cannons. comment out the danger indicator (ill use it later), every cannon shot must create a particle effect (SmokeSide should be mirror for right side cannon)
//psL1CannonMuzzle
var _ps = part_system_create();
part_system_draw_order(_ps, true);
//Muzzle
var _ptype1 = part_type_create();
part_type_sprite(_ptype1, texCannonMuzzle1, false, true, false)
part_type_size(_ptype1, 0.4, 0.6, 0.1, 0);
part_type_scale(_ptype1, 1.5, 1);
part_type_speed(_ptype1, 0, 0, 0, 0);
part_type_direction(_ptype1, 0, 360, 0, 0);
part_type_gravity(_ptype1, 0, 270);
part_type_orientation(_ptype1, 0, 0, 0, 0, false);
part_type_colour3(_ptype1, B2FEFF, $000000);
part_type_alpha3(_ptype1, 1, 1, 0);
part_type_blend(_ptype1, true);
part_type_life(_ptype1, 3, 5);
var _pemit1 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit1, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit1, _ptype1, 1);
//Sparks
var _ptype2 = part_type_create();
part_type_shape(_ptype2, pt_shape_flare);
part_type_size(_ptype2, 0.08, 0.1, 0, 0);
part_type_scale(_ptype2, 1, 1);
part_type_speed(_ptype2, 5, 12, -0.25, 0);
part_type_direction(_ptype2, -95, 95, 0, 0);
part_type_gravity(_ptype2, 0, 270);
part_type_orientation(_ptype2, 0, 360, 0, 0, false);
part_type_colour3(_ptype2, 0061FF, $00007F);
part_type_alpha3(_ptype2, 0, 1, 0);
part_type_blend(_ptype2, true);
part_type_life(_ptype2, 10, 15);
var _pemit2 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit2, 11, 17, -3, 3, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit2, _ptype2, 14);
//SmokeFast
var _ptype3 = part_type_create();
part_type_shape(_ptype3, pt_shape_cloud);
part_type_size(_ptype3, 0.4, 0.5, 0.03, 0);
part_type_scale(_ptype3, 1, 1);
part_type_speed(_ptype3, 3, 3.6, 0, 0);
part_type_direction(_ptype3, 0, 0, 0, 0);
part_type_gravity(_ptype3, 0, 270);
part_type_orientation(_ptype3, 0, 360, 0, 0, false);
part_type_colour3(_ptype3, 4C4C4C, $000000);
part_type_alpha3(_ptype3, 0.459, 1, 0);
part_type_blend(_ptype3, true);
part_type_life(_ptype3, 60, 70);
var _pemit3 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit3, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit3, _ptype3, 1);
//SmokeSide
var _ptype4 = part_type_create();
part_type_shape(_ptype4, pt_shape_cloud);
part_type_size(_ptype4, 0.15, 0.2, 0.025, 0);
part_type_scale(_ptype4, 1, 1);
part_type_speed(_ptype4, 1.8, 2, 0, 0);
part_type_direction(_ptype4, 90, 90, 0, 0);
part_type_gravity(_ptype4, 0, 90);
part_type_orientation(_ptype4, 0, 360, 0, 0, false);
part_type_colour3(_ptype4, 4C4C4C, $000000);
part_type_alpha3(_ptype4, 0.459, 1, 0);
part_type_blend(_ptype4, true);
part_type_life(_ptype4, 60, 70);
var _pemit4 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit4, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit4, _ptype4, 1);
//SmokeMed
var _ptype5 = part_type_create();
part_type_shape(_ptype5, pt_shape_cloud);
part_type_size(_ptype5, 0.15, 0.2, 0.025, 0);
part_type_scale(_ptype5, 1, 1);
part_type_speed(_ptype5, 1.8, 2, 0, 0);
part_type_direction(_ptype5, 0, 0, 0, 0);
part_type_gravity(_ptype5, 0, 270);
part_type_orientation(_ptype5, 0, 360, 0, 0, false);
part_type_colour3(_ptype5, 4C4C4C, $000000);
part_type_alpha3(_ptype5, 0.459, 1, 0);
part_type_blend(_ptype5, true);
part_type_life(_ptype5, 60, 70);
var _pemit5 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit5, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit5, _ptype5, 1);
//Flash
var _ptype6 = part_type_create();
part_type_shape(_ptype6, pt_shape_flare);
part_type_size(_ptype6, 1.4, 1.6, 0.1, 0);
part_type_scale(_ptype6, 1.2, 1);
part_type_speed(_ptype6, 0, 0, 0, 0);
part_type_direction(_ptype6, 0, 360, 0, 5);
part_type_gravity(_ptype6, 0, 270);
part_type_orientation(_ptype6, 90, 90, 0, 5, false);
part_type_colour3(_ptype6, 007DFF, $0000FF);
part_type_alpha3(_ptype6, 1, 1, 0);
part_type_blend(_ptype6, true);
part_type_life(_ptype6, 3, 5);
var _pemit6 = part_emitter_create(_ps);
part_emitter_region(_ps, _pemit6, 30.5, 31.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
part_emitter_burst(_ps, _pemit6, _ptype6, 1);
part_system_position(_ps, room_width/2, room_height/2);
Here are the changes you asked for, implemented cleanly:
closeDist instead of hovering at fireDist.Below are updated events for oEnemyL1.
gmlevent_inherited(); // Core Stats hp = real(80); infoName = "L1 'Hurricane'"; mass = 7000; threatAmount = 60; xpAmount = 150; circleRadius = 118; textOffset = -1280; armor = 4; ricochetMult = 20; sparksChance = 12; // Home / Patrol homeX = x; homeY = y; guardRadius = 7000; patrolTimer = 0; patrolIntervalMin = 120; patrolIntervalMax = 260; patrolTargetX = x; patrolTargetY = y; // Distances chaseDist = 4500; fireDist = 2600; closeDist = 1200; // Movement maxSpeed = 8; slowdown = 0.25; seekAccel = 1.9; patrolAccel = 0.55; brakeAccel = 1.1; avoidLookAhead = 240; avoidStrength = 2.4; separationRadius = 200; separationStrength = 1.0; // Strafing (rare bursts) strafeDir = choose(-1, 1); strafeActive = false; strafeActiveTimer = 0; strafeCooldown = irandom_range(240, 420); // time until next possible strafe strafeCooldownTimer = strafeCooldown; strafeBurstMin = 22; strafeBurstMax = 45; strafeStrength = 1.05; strafeChance = 0.18; // chance to start a strafe burst when cooldown ends // Targeting target = noone; targetDist = 999999; checkForFriendly = true; // --- Rockets (Barrage) --- rocketArmTime = 40; rocketVolleyDelay = 3; rocketReloadTime = 120; rocketMagazineReloadTime = 420; barrageShotsMax = 3; barrageShotsLeft = barrageShotsMax; rocketsPerSide = 6; rocketSpeed = 18; rocketSpread = 2; rocketOutShift = 10; rocketState = 0; // 0=ready, 1=arming, 2=volley, 3=reload, 4=magReload rocketTimer = 0; rocketVolleyTimer = 0; rocketVolleySide = 0; // 0=left, 1=right rocketVolleyRing = rocketsPerSide - 1; // inner -> outer dangerInst = noone; // --- Cannons (Close range) --- cannonRange = closeDist; cannonShotDelay = 4; cannonBurstReload = 28; cannonTimer = 0; cannonIndex = 0; // 0..5 (L,R,L,R,L,R) // --- Flares (kept) --- flareCooldown = 0; frMin = 5; frMax = 65; flareReactionTime = random_range(frMin, frMax); canDeployFlares = true; hasFlares = true; flaresAmount = 12; if (hasFlares) { flaresAmmo = 3; } else { flaresAmmo = 0; } // Sprite sprite_index = texEnemyL1; image_speed = 0; // Helpers getWorldFromSprite = function(_sx, _sy) { var dx = _sx - sprite_get_xoffset(texEnemyL1); var dy = _sy - sprite_get_yoffset(texEnemyL1); var wx = x + lengthdir_x(dx, image_angle) + lengthdir_x(dy, image_angle + 90); var wy = y + lengthdir_y(dx, image_angle) + lengthdir_y(dy, image_angle + 90); return [ wx, wy ]; }; pickNewPatrolPoint = function() { var ang = random(360); var rad = random_range(800, guardRadius); patrolTargetX = homeX + lengthdir_x(rad, ang); patrolTargetY = homeY + lengthdir_y(rad, ang); patrolTimer = irandom_range(patrolIntervalMin, patrolIntervalMax); }; getBarragePoint = function(_side, _ringIndex) { var t = 0; if (rocketsPerSide > 1) { t = _ringIndex / (rocketsPerSide - 1); } var sx1, sy1, sx2, sy2; var sideSign; if (_side == 0) { sx1 = 70; sy1 = 14; sx2 = 86; sy2 = 32; sideSign = -1; } else { sx1 = 70; sy1 = 114; sx2 = 86; sy2 = 96; sideSign = 1; } var sx = lerp(sx1, sx2, t); var sy = lerp(sy1, sy2, t); var p = getWorldFromSprite(sx, sy); var wx = p[0]; var wy = p[1]; var normalAng = image_angle + 90; wx += lengthdir_x(rocketOutShift * sideSign, normalAng); wy += lengthdir_y(rocketOutShift * sideSign, normalAng); return [ wx, wy ]; }; spawnRocketAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY) + random_range(-rocketSpread, rocketSpread); var r = instance_create_layer(_wx, _wy, "Instances", oRocket1); with (r) { hostile = true; direction = dir; image_angle = dir; speed = other.rocketSpeed; hspeed += other.hspeed; vspeed += other.vspeed; } }; spawnCannonAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY); var b = instance_create_layer(_wx, _wy, "Instances", oCannonBullet); with (b) { hostile = true; direction = dir; image_angle = dir; speed = 55; hspeed += other.hspeed; vspeed += other.vspeed; } }; // Danger indicator (commented for later use) startArming = function() { rocketState = 1; rocketTimer = rocketArmTime; /* if (!instance_exists(dangerInst)) { dangerInst = instance_create_layer(x, y, "Instances", oEnemyL1Danger); with (dangerInst) { parentShip = other.id; } } */ }; startVolley = function() { rocketState = 2; rocketVolleyTimer = 0; rocketVolleySide = 0; rocketVolleyRing = rocketsPerSide - 1; /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; */ }; endBarrageAndReload = function() { barrageShotsLeft -= 1; if (barrageShotsLeft > 0) { rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketState = 4; rocketTimer = rocketMagazineReloadTime; } }; // ---------------------------------------------------- // Cannon muzzle particle system (created once) // ---------------------------------------------------- psL1CannonMuzzle = part_system_create(); part_system_draw_order(psL1CannonMuzzle, true); // Muzzle ptL1Muzzle = part_type_create(); part_type_sprite(ptL1Muzzle, texCannonMuzzle1, false, true, false); part_type_size(ptL1Muzzle, 0.4, 0.6, 0.1, 0); part_type_scale(ptL1Muzzle, 1.5, 1); part_type_speed(ptL1Muzzle, 0, 0, 0, 0); part_type_direction(ptL1Muzzle, 0, 360, 0, 0); part_type_gravity(ptL1Muzzle, 0, 270); part_type_orientation(ptL1Muzzle, 0, 0, 0, 0, false); part_type_colour3(ptL1Muzzle, $E5FAFF, $B2FEFF, $000000); part_type_alpha3(ptL1Muzzle, 1, 1, 0); part_type_blend(ptL1Muzzle, true); part_type_life(ptL1Muzzle, 3, 5); // Sparks ptL1Sparks = part_type_create(); part_type_shape(ptL1Sparks, pt_shape_flare); part_type_size(ptL1Sparks, 0.08, 0.1, 0, 0); part_type_scale(ptL1Sparks, 1, 1); part_type_speed(ptL1Sparks, 5, 12, -0.25, 0); part_type_direction(ptL1Sparks, -95, 95, 0, 0); part_type_gravity(ptL1Sparks, 0, 270); part_type_orientation(ptL1Sparks, 0, 360, 0, 0, false); part_type_colour3(ptL1Sparks, $FFFFFF, $0061FF, $00007F); part_type_alpha3(ptL1Sparks, 0, 1, 0); part_type_blend(ptL1Sparks, true); part_type_life(ptL1Sparks, 10, 15); // SmokeFast ptL1SmokeFast = part_type_create(); part_type_shape(ptL1SmokeFast, pt_shape_cloud); part_type_size(ptL1SmokeFast, 0.4, 0.5, 0.03, 0); part_type_scale(ptL1SmokeFast, 1, 1); part_type_speed(ptL1SmokeFast, 3, 3.6, 0, 0); part_type_direction(ptL1SmokeFast, 0, 0, 0, 0); part_type_gravity(ptL1SmokeFast, 0, 270); part_type_orientation(ptL1SmokeFast, 0, 360, 0, 0, false); part_type_colour3(ptL1SmokeFast, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(ptL1SmokeFast, 0.459, 1, 0); part_type_blend(ptL1SmokeFast, true); part_type_life(ptL1SmokeFast, 60, 70); // SmokeSide (Left) ptL1SmokeSideL = part_type_create(); part_type_shape(ptL1SmokeSideL, pt_shape_cloud); part_type_size(ptL1SmokeSideL, 0.15, 0.2, 0.025, 0); part_type_scale(ptL1SmokeSideL, 1, 1); part_type_speed(ptL1SmokeSideL, 1.8, 2, 0, 0); part_type_direction(ptL1SmokeSideL, 90, 90, 0, 0); part_type_gravity(ptL1SmokeSideL, 0, 90); part_type_orientation(ptL1SmokeSideL, 0, 360, 0, 0, false); part_type_colour3(ptL1SmokeSideL, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(ptL1SmokeSideL, 0.459, 1, 0); part_type_blend(ptL1SmokeSideL, true); part_type_life(ptL1SmokeSideL, 60, 70); // SmokeSide (Right mirrored) ptL1SmokeSideR = part_type_create(); part_type_shape(ptL1SmokeSideR, pt_shape_cloud); part_type_size(ptL1SmokeSideR, 0.15, 0.2, 0.025, 0); part_type_scale(ptL1SmokeSideR, 1, 1); part_type_speed(ptL1SmokeSideR, 1.8, 2, 0, 0); part_type_direction(ptL1SmokeSideR, 270, 270, 0, 0); part_type_gravity(ptL1SmokeSideR, 0, 90); part_type_orientation(ptL1SmokeSideR, 0, 360, 0, 0, false); part_type_colour3(ptL1SmokeSideR, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(ptL1SmokeSideR, 0.459, 1, 0); part_type_blend(ptL1SmokeSideR, true); part_type_life(ptL1SmokeSideR, 60, 70); // SmokeMed ptL1SmokeMed = part_type_create(); part_type_shape(ptL1SmokeMed, pt_shape_cloud); part_type_size(ptL1SmokeMed, 0.15, 0.2, 0.025, 0); part_type_scale(ptL1SmokeMed, 1, 1); part_type_speed(ptL1SmokeMed, 1.8, 2, 0, 0); part_type_direction(ptL1SmokeMed, 0, 0, 0, 0); part_type_gravity(ptL1SmokeMed, 0, 270); part_type_orientation(ptL1SmokeMed, 0, 360, 0, 0, false); part_type_colour3(ptL1SmokeMed, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(ptL1SmokeMed, 0.459, 1, 0); part_type_blend(ptL1SmokeMed, true); part_type_life(ptL1SmokeMed, 60, 70); // Flash ptL1Flash = part_type_create(); part_type_shape(ptL1Flash, pt_shape_flare); part_type_size(ptL1Flash, 1.4, 1.6, 0.1, 0); part_type_scale(ptL1Flash, 1.2, 1); part_type_speed(ptL1Flash, 0, 0, 0, 0); part_type_direction(ptL1Flash, 0, 360, 0, 5); part_type_gravity(ptL1Flash, 0, 270); part_type_orientation(ptL1Flash, 90, 90, 0, 5, false); part_type_colour3(ptL1Flash, $0055FF, $007DFF, $0000FF); part_type_alpha3(ptL1Flash, 1, 1, 0); part_type_blend(ptL1Flash, true); part_type_life(ptL1Flash, 3, 5); // Reusable emitters (region kept tiny; we position system at muzzle) emL1Muzzle = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1Muzzle, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear); emL1Sparks = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1Sparks, 11, 17, -3, 3, ps_shape_ellipse, ps_distr_linear); emL1SmokeFast = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1SmokeFast, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear); emL1SmokeSide = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1SmokeSide, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear); emL1SmokeMed = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1SmokeMed, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear); emL1Flash = part_emitter_create(psL1CannonMuzzle); part_emitter_region(psL1CannonMuzzle, emL1Flash, 30.5, 31.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear); cannonMuzzleBurst = function(_wx, _wy, _isRight) { part_system_position(psL1CannonMuzzle, _wx, _wy); part_emitter_burst(psL1CannonMuzzle, emL1Muzzle, ptL1Muzzle, 1); part_emitter_burst(psL1CannonMuzzle, emL1Sparks, ptL1Sparks, 14); part_emitter_burst(psL1CannonMuzzle, emL1SmokeFast, ptL1SmokeFast, 1); if (_isRight) { part_emitter_burst(psL1CannonMuzzle, emL1SmokeSide, ptL1SmokeSideR, 1); } else { part_emitter_burst(psL1CannonMuzzle, emL1SmokeSide, ptL1SmokeSideL, 1); } part_emitter_burst(psL1CannonMuzzle, emL1SmokeMed, ptL1SmokeMed, 1); part_emitter_burst(psL1CannonMuzzle, emL1Flash, ptL1Flash, 1); };
gmlevent_inherited(); sprite_index = texEnemyL1; image_speed = 0; // Speed clamp + slowdown speed = clamp(speed, 0, maxSpeed); if (speed > 0) { speed -= slowdown; if (speed < 0) { speed = 0; } } // Target acquire if (instance_exists(oPlayer) && global.playerExists) { target = oPlayer; targetDist = point_distance(x, y, target.x, target.y); checkForFriendly = !collision_line(x, y, target.x, target.y, grpHostiles, true, true); } else { target = noone; targetDist = 999999; checkForFriendly = true; } // Patrol point management if (patrolTimer <= 0) { pickNewPatrolPoint(); } else { patrolTimer -= 1; } // Rare strafe logic (no zig-zag) // - cooldown counts down // - when reaches 0, we MAY start a short burst if (!strafeActive) { strafeCooldownTimer -= 1; if (strafeCooldownTimer <= 0) { if (random(1) < strafeChance) { strafeActive = true; strafeDir = choose(-1, 1); strafeActiveTimer = irandom_range(strafeBurstMin, strafeBurstMax); } strafeCooldownTimer = irandom_range(240, 420); } } else { strafeActiveTimer -= 1; if (strafeActiveTimer <= 0) { strafeActive = false; } } // Separation from other hostiles (light) if (distance_to_object(grpHostiles) < separationRadius) { var inst = instance_place(x, y, grpHostiles); if (inst != noone) { var diffX = x - inst.x; var diffY = y - inst.y; var len = max(1, point_distance(0, 0, diffX, diffY)); diffX /= len; diffY /= len; hspeed += diffX * separationStrength; vspeed += diffY * separationStrength; } } // Collision avoidance vs grpCollide var avoidX = 0; var avoidY = 0; var lookX = x + lengthdir_x(avoidLookAhead, image_angle); var lookY = y + lengthdir_y(avoidLookAhead, image_angle); if (collision_line(x, y, lookX, lookY, grpCollide, true, true)) { avoidX += lengthdir_x(avoidStrength, image_angle + 90); avoidY += lengthdir_y(avoidStrength, image_angle + 90); } // Mode flags var inChase = (target != noone && targetDist < chaseDist); var inFire = (target != noone && targetDist < fireDist); var inClose = (target != noone && targetDist < closeDist); // Lead aim points (same fix as before) var rocketAimX = 0; var rocketAimY = 0; var cannonAimX = 0; var cannonAimY = 0; if (target != noone) { var tR = targetDist / max(1, rocketSpeed); rocketAimX = target.x + (target.hspeed - hspeed) * tR; rocketAimY = target.y + (target.vspeed - vspeed) * tR; var cannonBulletSpeed = 55; var tC = targetDist / max(1, cannonBulletSpeed); cannonAimX = target.x + (target.hspeed - hspeed) * tC; cannonAimY = target.y + (target.vspeed - vspeed) * tC; // Rotate toward lead (visual intelligence) var lookDir = point_direction(x, y, rocketAimX, rocketAimY); var rotFactor = -4.5; image_angle += sin(degtorad(image_angle - lookDir)) * rotFactor; } else { var pDir = point_direction(x, y, patrolTargetX, patrolTargetY); var pRot = -2.8; image_angle += sin(degtorad(image_angle - pDir)) * pRot; } // Movement if (!inChase) { // Patrol to patrol point var pMoveDir = point_direction(x, y, patrolTargetX, patrolTargetY); motion_add(pMoveDir, patrolAccel); hspeed += avoidX; vspeed += avoidY; } else { // IMPORTANT CHANGE: // If we can cannon, we WANT to close in hard to closeDist. // If not cannon-range yet, we still push in (not hovering at fireDist). var toTargetDir = point_direction(x, y, target.x, target.y); if (!inClose) { // Push into close range so cannons actually happen motion_add(toTargetDir, seekAccel); } else { // In close range, avoid ramming too hard if (targetDist < closeDist - 180) { motion_add(toTargetDir + 180, brakeAccel); } } // Only strafe rarely AND only while in firing zone if (inFire && strafeActive) { motion_add(toTargetDir + (90 * strafeDir), strafeStrength); } hspeed += avoidX; vspeed += avoidY; } // --- Rockets (unchanged ordering volley, danger indicator commented) --- var rocketOk = ( target != noone && inFire && !inClose && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (rocketState == 0) { if (rocketOk) { startArming(); } } else if (rocketState == 1) { rocketTimer -= 1; if (!rocketOk) { rocketState = 0; rocketTimer = 0; /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; */ } else if (rocketTimer <= 0) { startVolley(); } } else if (rocketState == 2) { if (!rocketOk) { rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketVolleyTimer -= 1; if (rocketVolleyTimer <= 0) { if (rocketVolleyRing >= 0) { var p = getBarragePoint(rocketVolleySide, rocketVolleyRing); spawnRocketAt(p[0], p[1], rocketAimX, rocketAimY); if (rocketVolleySide == 0) { rocketVolleySide = 1; } else { rocketVolleySide = 0; rocketVolleyRing -= 1; } rocketVolleyTimer = rocketVolleyDelay; } else { endBarrageAndReload(); } } } } else if (rocketState == 3) { rocketTimer -= 1; if (rocketTimer <= 0) { rocketState = 0; rocketTimer = 0; } } else if (rocketState == 4) { rocketTimer -= 1; if (rocketTimer <= 0) { barrageShotsLeft = barrageShotsMax; rocketState = 0; rocketTimer = 0; } } // --- Cannons (must always create particles) --- var cannonOk = ( target != noone && inClose && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (cannonOk) { if (cannonTimer <= 0) { if ((cannonIndex % 2) == 0) { var pL = getWorldFromSprite(107, 50); spawnCannonAt(pL[0], pL[1], cannonAimX, cannonAimY); cannonMuzzleBurst(pL[0], pL[1], false); } else { var pR = getWorldFromSprite(107, 78); spawnCannonAt(pR[0], pR[1], cannonAimX, cannonAimY); cannonMuzzleBurst(pR[0], pR[1], true); } cannonIndex += 1; if (cannonIndex >= 6) { cannonIndex = 0; cannonTimer = cannonBurstReload; } else { cannonTimer = cannonShotDelay; } } } if (cannonTimer > 0) { cannonTimer -= 1; } // --- Flares (kept) --- if (hasFlares && flaresAmmo > 0 && canDeployFlares) { var shouldDeployFlares = false; with (oMissilePhase2) { var distToMissile = point_distance(other.x, other.y, x, y); if (distToMissile < 900) { shouldDeployFlares = true; break; } } if (shouldDeployFlares) { flareCooldown += 1; if (flareCooldown >= flareReactionTime) { DeployFlares(); flareCooldown = 0; flareReactionTime = random_range(frMin, frMax); } } else { flareCooldown = 0; } }
gmlcanDeployFlares = true;
gmlif (part_system_exists(psL1CannonMuzzle)) { part_type_destroy(ptL1Muzzle); part_type_destroy(ptL1Sparks); part_type_destroy(ptL1SmokeFast); part_type_destroy(ptL1SmokeSideL); part_type_destroy(ptL1SmokeSideR); part_type_destroy(ptL1SmokeMed); part_type_destroy(ptL1Flash); part_system_destroy(psL1CannonMuzzle); }
strafeChance = 0.10; (very rare)strafeCooldownTimer = irandom_range(360, 600); (longer downtime)strafeBurstMin/Max (shorter bursts)If you want, I can also add “only strafe when the player is aiming at it / shooting” (reads player weapon state or incoming bullets) so strafing feels reactive instead of random.
particles must spawn facing image_angle at muzzle point (maybe use local particle system, script or directly in event, depending on which is better for performance with multiple objects of this type). cannons must have a max distance, so i can set up closeDist = 800, and shootDist = 1200. also think of new ideas for this ship
Absolutely — here’s the clean, performance-friendly way to do it:
✅ Particles spawn at the muzzle point, rotated to image_angle
✅ SmokeSide mirrors automatically (right cannon flips)
✅ Uses ONE shared particle system + particle types globally (best performance when many L1 ships exist)
✅ Cannons now have:
closeDist = 800 (preferred range to close in)shootDist = 1200 (max cannon firing distance)✅ Strafing is rare (burst + cooldown, only in combat)
And at the end I’ll give you new gameplay ideas for L1 that fit your systems.
scL1CannonParticlesInitCall once in your controller / room start / game init.
(If you already have a particle manager object, put it there.)
gml/// scL1CannonParticlesInit() if (variable_global_exists("psL1CannonMuzzle")) { exit; } global.psL1CannonMuzzle = part_system_create(); part_system_draw_order(global.psL1CannonMuzzle, true); // ---------------------------------------------------- // Muzzle // ---------------------------------------------------- global.ptL1Muzzle = part_type_create(); part_type_sprite(global.ptL1Muzzle, texCannonMuzzle1, false, true, false); part_type_size(global.ptL1Muzzle, 0.4, 0.6, 0.1, 0); part_type_scale(global.ptL1Muzzle, 1.5, 1); part_type_speed(global.ptL1Muzzle, 0, 0, 0, 0); part_type_direction(global.ptL1Muzzle, 0, 360, 0, 0); part_type_gravity(global.ptL1Muzzle, 0, 270); part_type_orientation(global.ptL1Muzzle, 0, 0, 0, 0, false); part_type_colour3(global.ptL1Muzzle, $E5FAFF, $B2FEFF, $000000); part_type_alpha3(global.ptL1Muzzle, 1, 1, 0); part_type_blend(global.ptL1Muzzle, true); part_type_life(global.ptL1Muzzle, 3, 5); // ---------------------------------------------------- // Sparks // ---------------------------------------------------- global.ptL1Sparks = part_type_create(); part_type_shape(global.ptL1Sparks, pt_shape_flare); part_type_size(global.ptL1Sparks, 0.08, 0.1, 0, 0); part_type_scale(global.ptL1Sparks, 1, 1); part_type_speed(global.ptL1Sparks, 5, 12, -0.25, 0); part_type_direction(global.ptL1Sparks, -95, 95, 0, 0); part_type_gravity(global.ptL1Sparks, 0, 270); part_type_orientation(global.ptL1Sparks, 0, 360, 0, 0, false); part_type_colour3(global.ptL1Sparks, $FFFFFF, $0061FF, $00007F); part_type_alpha3(global.ptL1Sparks, 0, 1, 0); part_type_blend(global.ptL1Sparks, true); part_type_life(global.ptL1Sparks, 10, 15); // ---------------------------------------------------- // SmokeFast // ---------------------------------------------------- global.ptL1SmokeFast = part_type_create(); part_type_shape(global.ptL1SmokeFast, pt_shape_cloud); part_type_size(global.ptL1SmokeFast, 0.4, 0.5, 0.03, 0); part_type_scale(global.ptL1SmokeFast, 1, 1); part_type_speed(global.ptL1SmokeFast, 3, 3.6, 0, 0); part_type_direction(global.ptL1SmokeFast, 0, 0, 0, 0); part_type_gravity(global.ptL1SmokeFast, 0, 270); part_type_orientation(global.ptL1SmokeFast, 0, 360, 0, 0, false); part_type_colour3(global.ptL1SmokeFast, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(global.ptL1SmokeFast, 0.459, 1, 0); part_type_blend(global.ptL1SmokeFast, true); part_type_life(global.ptL1SmokeFast, 60, 70); // ---------------------------------------------------- // SmokeSide (we will rotate to image_angle +/- 90) // ---------------------------------------------------- global.ptL1SmokeSide = part_type_create(); part_type_shape(global.ptL1SmokeSide, pt_shape_cloud); part_type_size(global.ptL1SmokeSide, 0.15, 0.2, 0.025, 0); part_type_scale(global.ptL1SmokeSide, 1, 1); part_type_speed(global.ptL1SmokeSide, 1.8, 2, 0, 0); part_type_direction(global.ptL1SmokeSide, 0, 0, 0, 0); // we orient it manually part_type_gravity(global.ptL1SmokeSide, 0, 90); part_type_orientation(global.ptL1SmokeSide, 0, 360, 0, 0, false); part_type_colour3(global.ptL1SmokeSide, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(global.ptL1SmokeSide, 0.459, 1, 0); part_type_blend(global.ptL1SmokeSide, true); part_type_life(global.ptL1SmokeSide, 60, 70); // ---------------------------------------------------- // SmokeMed // ---------------------------------------------------- global.ptL1SmokeMed = part_type_create(); part_type_shape(global.ptL1SmokeMed, pt_shape_cloud); part_type_size(global.ptL1SmokeMed, 0.15, 0.2, 0.025, 0); part_type_scale(global.ptL1SmokeMed, 1, 1); part_type_speed(global.ptL1SmokeMed, 1.8, 2, 0, 0); part_type_direction(global.ptL1SmokeMed, 0, 0, 0, 0); part_type_gravity(global.ptL1SmokeMed, 0, 270); part_type_orientation(global.ptL1SmokeMed, 0, 360, 0, 0, false); part_type_colour3(global.ptL1SmokeMed, $4C4C4C, $4C4C4C, $000000); part_type_alpha3(global.ptL1SmokeMed, 0.459, 1, 0); part_type_blend(global.ptL1SmokeMed, true); part_type_life(global.ptL1SmokeMed, 60, 70); // ---------------------------------------------------- // Flash // ---------------------------------------------------- global.ptL1Flash = part_type_create(); part_type_shape(global.ptL1Flash, pt_shape_flare); part_type_size(global.ptL1Flash, 1.4, 1.6, 0.1, 0); part_type_scale(global.ptL1Flash, 1.2, 1); part_type_speed(global.ptL1Flash, 0, 0, 0, 0); part_type_direction(global.ptL1Flash, 0, 360, 0, 5); part_type_gravity(global.ptL1Flash, 0, 270); part_type_orientation(global.ptL1Flash, 90, 90, 0, 5, false); part_type_colour3(global.ptL1Flash, $0055FF, $007DFF, $0000FF); part_type_alpha3(global.ptL1Flash, 1, 1, 0); part_type_blend(global.ptL1Flash, true); part_type_life(global.ptL1Flash, 3, 5); // Reusable emitter global.emL1Cannon = part_emitter_create(global.psL1CannonMuzzle); part_emitter_region(global.psL1CannonMuzzle, global.emL1Cannon, -0.5, 0.5, -0.5, 0.5, ps_shape_ellipse, ps_distr_linear);
scL1CannonMuzzleBurstThis is called each cannon shot.
It positions the global system at the muzzle and spawns particles facing image_angle.
gml/// scL1CannonMuzzleBurst(xPos, yPos, facingAngle, isRight) var xPos = argument0; var yPos = argument1; var facingAngle = argument2; var isRight = argument3; if (!variable_global_exists("psL1CannonMuzzle")) { exit; } part_system_position(global.psL1CannonMuzzle, xPos, yPos); // Muzzle / Flash oriented forward part_type_direction(global.ptL1Muzzle, facingAngle - 2, facingAngle + 2, 0, 0); part_type_direction(global.ptL1Flash, facingAngle, facingAngle, 0, 0); part_type_direction(global.ptL1SmokeFast, facingAngle, facingAngle, 0, 0); part_type_direction(global.ptL1SmokeMed, facingAngle, facingAngle, 0, 0); // Sparks are symmetric spread around facing part_type_direction(global.ptL1Sparks, facingAngle - 95, facingAngle + 95, 0, 0); // SmokeSide: mirror var sideAngle = facingAngle + 90; if (isRight) { sideAngle = facingAngle - 90; } part_type_direction(global.ptL1SmokeSide, sideAngle, sideAngle, 0, 0); // Bursts part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1Muzzle, 1); part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1Sparks, 14); part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1SmokeFast, 1); part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1SmokeSide, 1); part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1SmokeMed, 1); part_emitter_burst(global.psL1CannonMuzzle, global.emL1Cannon, global.ptL1Flash, 1);
✅ This is the fastest approach:
part_system_position + bursts.Add these:
gmlcloseDist = 800; // preferred “want-to-be” distance shootDist = 1200; // cannon max firing distance
Everything else you already have stays, but replace any closeDist uses with the new intent (below).
This is the important behavior fix:
✅ It pushes into closeDist
✅ It will not hover at fireDist while cannons exist
✅ It will still use rockets when between shootDist and fireDist
Replace your combat movement section with this:
gmlvar inChase = (target != noone && targetDist < chaseDist); var inFire = (target != noone && targetDist < fireDist); var inShoot = (target != noone && targetDist < shootDist); var tooClose = (target != noone && targetDist < closeDist - 160); var wantClose = (target != noone && targetDist > closeDist + 120); if (!inChase) { var pMoveDir = point_direction(x, y, patrolTargetX, patrolTargetY); motion_add(pMoveDir, patrolAccel); hspeed += avoidX; vspeed += avoidY; } else { var toTargetDir = point_direction(x, y, target.x, target.y); // Close in to preferred cannon distance if (wantClose) { motion_add(toTargetDir, seekAccel); } else if (tooClose) { motion_add(toTargetDir + 180, brakeAccel); } // Rare strafe only when within shoot band (not always) if (inShoot && strafeActive) { motion_add(toTargetDir + (90 * strafeDir), strafeStrength); } hspeed += avoidX; vspeed += avoidY; }
Replace your cannon logic with this:
gmlvar cannonOk = ( target != noone && targetDist < shootDist && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (cannonOk) { if (cannonTimer <= 0) { if ((cannonIndex % 2) == 0) { var pL = getWorldFromSprite(107, 50); spawnCannonAt(pL[0], pL[1], cannonAimX, cannonAimY); scL1CannonMuzzleBurst(pL[0], pL[1], image_angle, false); } else { var pR = getWorldFromSprite(107, 78); spawnCannonAt(pR[0], pR[1], cannonAimX, cannonAimY); scL1CannonMuzzleBurst(pR[0], pR[1], image_angle, true); } cannonIndex += 1; if (cannonIndex >= 6) { cannonIndex = 0; cannonTimer = cannonBurstReload; } else { cannonTimer = cannonShotDelay; } } } if (cannonTimer > 0) { cannonTimer -= 1; }
✅ Cannon particles now spawn:
image_angleCall scL1CannonParticlesInit() once in one of these spots:
oGameManager Create Event
gmlscL1CannonParticlesInit();
Room Start event of your world room.
That ensures the global particle resources exist before any L1 shoots.
Here are mechanics that match your current systems (threat level, modules, gas cloud, wormhole, open world):
When L1 arms rockets, it creates a temporary targeting field:
Counterplay: Shield blocks / flare / break line-of-sight.
Instead of stopping close:
closeDistThis creates a distinctive heavy ship that doesn’t just sit in your face.
(If you want, I can code this as a “attackRun” mode with a loop point.)
Because it’s heavy:
This makes it feel like you’re peeling it apart.
Instead of rockets aimed at player, rockets fire in a slightly arcing curtain:
If the L1 enters gas cloud:
This makes hazards feel alive and improves sandbox combat.
When L1 exists, smaller enemies behave differently:
If you want, I can also:
✅ convert its AI into a clean finite state machine (Patrol / Chase / StrafeBurst / CannonEngage / RocketEngage / Retreat)
✅ add drive-by attack runs (super cool for a big ship)
✅ make rockets use a “fan” pattern based on player movement direction (more intelligent and readable)
Great! We will add armor plates mechanic (i will draw sprites for them, they will all sit at x y of the ship (the sprite will have a set-up anchor offset and own collision) and rotate with its image_angle, armor should be handled in End Step Event
Armor plate is a separate object with hp, armor and ricochetMult, it will be reused by different ships so those vars and its sprite should be adjustable. once the armor plate runs out of hp its destroyed with a particle effect (part_particles_burst(global.partSystem, x, y, psShipExplode);)
Also lets add a FOV, when created the enemy is patrolling the world, if the player gets inside its fov it will engage, when the player is outside fov it follow last known coordinates and starts a player lost countdown after which resets back to patrolling
Alright — we’ll add this in a way that stays reusable, simple, and fast with lots of ships:
oArmorPlate is a generic plate object (configurable hp/maxHp, armor, ricochetMult, sprite_index, parentShip).image_angle (no offsets required).oEnemyL1 gets FOV + last known position + player-lost countdown.Below is the code.
gmlevent_inherited(); parentShip = noone; // Configurable stats (set these right after creating the plate) maxHp = 20; hp = maxHp; plateArmor = 2; plateRicochetMult = 10; // Optional: plates can be non-solid but still collide with bullets depending on your bullet logic image_speed = 0;
gmlevent_inherited(); if (!instance_exists(parentShip)) { instance_destroy(); exit; } // Sit on parent and rotate with it x = parentShip.x; y = parentShip.y; image_angle = parentShip.image_angle;
gmlevent_inherited(); // Only explode if it actually "broke" if (hp <= 0) { part_particles_burst(global.partSystem, x, y, psShipExplode); }
Damage handling note: your bullets/rockets should damage
oArmorPlatewhen they collide with it (since it has its own collision mask). Keep damage logic where it already exists (bullet collision event, etc.).
gmlevent_inherited(); // Base stats (unchanged conceptually) baseArmor = 4; baseRicochetMult = 20; // These get overwritten in End Step based on plates armor = baseArmor; ricochetMult = baseRicochetMult; // ---------------------------------------------------- // FOV + detection // ---------------------------------------------------- fovDist = 3800; fovHalfAngle = 55; // total FOV = 110 degrees playerSeen = false; playerLostTimerMax = 240; // frames to search after losing FOV playerLostTimer = 0; lastKnownX = x; lastKnownY = y; // ---------------------------------------------------- // Patrol / home (if you already have these, keep yours) // ---------------------------------------------------- homeX = x; homeY = y; guardRadius = 7000; patrolTimer = 0; patrolIntervalMin = 120; patrolIntervalMax = 260; patrolTargetX = x; patrolTargetY = y; // ---------------------------------------------------- // Cannon distances (as requested) // ---------------------------------------------------- closeDist = 800; // desired distance shootDist = 1200; // max firing distance for cannons // ---------------------------------------------------- // Armor plates setup // ---------------------------------------------------- // Create plates that sit at ship x/y and rotate with it. // Set each plate's sprite/stats here. // (You can add more plates later; this is intentionally simple.) plateCount = 2; platesAlive = 0; plateA = instance_create_layer(x, y, "Instances", oArmorPlate); with (plateA) { parentShip = other.id; sprite_index = texL1PlateA; // <--- you will create this image_angle = other.image_angle; maxHp = 28; hp = maxHp; plateArmor = 2; plateRicochetMult = 10; } platesAlive += 1; plateB = instance_create_layer(x, y, "Instances", oArmorPlate); with (plateB) { parentShip = other.id; sprite_index = texL1PlateB; // <--- you will create this image_angle = other.image_angle; maxHp = 28; hp = maxHp; plateArmor = 2; plateRicochetMult = 10; } platesAlive += 1; // If you want “broken plates = more aggressive”, we track it: platesBroken = 0;
Replace
texL1PlateA/Bwith whatever sprite names you choose.
This assumes you already have your movement + rockets + cannons code; you’ll gate combat logic using playerSeen and playerLostTimer.
Add/replace your target acquisition block with this:
gmlevent_inherited(); // --- Find player --- var playerExists = instance_exists(oPlayer) && global.playerExists; if (!playerExists) { playerSeen = false; playerLostTimer = 0; target = noone; } else { target = oPlayer; var dx = target.x - x; var dy = target.y - y; var dist = point_distance(0, 0, dx, dy); // FOV check (distance + angle vs current facing) var dirToPlayer = point_direction(x, y, target.x, target.y); var angDiff = abs(angle_difference(image_angle, dirToPlayer)); var inFov = (dist <= fovDist) && (angDiff <= fovHalfAngle); if (inFov) { playerSeen = true; playerLostTimer = playerLostTimerMax; lastKnownX = target.x; lastKnownY = target.y; } else { if (playerSeen) { playerLostTimer -= 1; if (playerLostTimer <= 0) { playerSeen = false; target = noone; // Reset to patrol behavior patrolTimer = 0; } } } } // ---------------------------------------------------- // Decide behavior mode // ---------------------------------------------------- var engaging = playerSeen; var searching = (!playerSeen && playerLostTimer > 0); var patrolling = (!playerSeen && playerLostTimer <= 0); // Your collision avoidance / separation can still run always // ---------------------------------------------------- // Movement target selection // ---------------------------------------------------- if (patrolling) { // Use your patrol logic (wander point) // Example (keep your own if you already have it): if (patrolTimer <= 0) { var ang = random(360); var rad = random_range(800, guardRadius); patrolTargetX = homeX + lengthdir_x(rad, ang); patrolTargetY = homeY + lengthdir_y(rad, ang); patrolTimer = irandom_range(patrolIntervalMin, patrolIntervalMax); } else { patrolTimer -= 1; } var pDir = point_direction(x, y, patrolTargetX, patrolTargetY); motion_add(pDir, patrolAccel); } else if (searching) { // Go to last known position var sDir = point_direction(x, y, lastKnownX, lastKnownY); motion_add(sDir, seekAccel * 0.85); } else if (engaging) { // ---------------------------------------------------- // Combat movement: CLOSE IN to closeDist (so cannons happen) // ---------------------------------------------------- var distToTarget = point_distance(x, y, target.x, target.y); var toTargetDir = point_direction(x, y, target.x, target.y); var wantClose = (distToTarget > closeDist + 120); var tooClose = (distToTarget < closeDist - 160); if (wantClose) { motion_add(toTargetDir, seekAccel); } else if (tooClose) { motion_add(toTargetDir + 180, brakeAccel); } // Rare strafe ONLY when within shoot band if (distToTarget <= shootDist && strafeActive) { motion_add(toTargetDir + (90 * strafeDir), strafeStrength); } } // ---------------------------------------------------- // Combat gates // ---------------------------------------------------- // IMPORTANT: only allow weapons while engaging or searching (optional). // Usually: weapons only while engaging (playerSeen). var weaponsAllowed = engaging; // Use weaponsAllowed to gate rockets/cannons: // - Rockets: weaponsAllowed && dist < fireDist && dist > shootDist (or > closeDist) // - Cannons: weaponsAllowed && dist < shootDist
Use these bands:
dist <= shootDistshootDist < dist <= fireDistThat produces “close to cannon, mid to rockets”.
This is what you asked: the ship’s armor/ricochetMult is computed in End Step, based on live plates.
gmlevent_inherited(); // Aggregate plate stats (End Step so all plate damage/collisions already happened) var totalPlateArmor = 0; var totalPlateRicochet = 0; var alive = 0; // We created plateA/plateB references in Create. // If you later move to arrays/lists, this is the only block you’d refactor. if (instance_exists(plateA)) { totalPlateArmor += plateA.plateArmor; totalPlateRicochet += plateA.plateRicochetMult; alive += 1; } else { // Count broken once if (plateA != noone) { plateA = noone; platesBroken += 1; } } if (instance_exists(plateB)) { totalPlateArmor += plateB.plateArmor; totalPlateRicochet += plateB.plateRicochetMult; alive += 1; } else { if (plateB != noone) { plateB = noone; platesBroken += 1; } } platesAlive = alive; // Apply totals armor = baseArmor + totalPlateArmor; ricochetMult = baseRicochetMult + totalPlateRicochet; // Optional: when all plates are gone, it becomes nastier (identity reward) // Keep it very simple and deterministic. if (platesAlive <= 0) { cannonShotDelay = 3; rocketVolleyDelay = 2; } else { cannonShotDelay = 4; rocketVolleyDelay = 3; }
Give each plate a different collision zone:
During searching, have it do a slow spiral or triangular sweep around lastKnownX/Y.
This makes “breaking sight” feel like stealth gameplay without adding new systems.
When a plate breaks:
closeDist harder)
This gives a clear phase change that players can read.If it enters Gas Cloud (modules disabled):
fovHalfAngle *= 0.6)While plates are alive, it buffs nearby S1/M2 accuracy slightly. When plates break, buff ends → “remove armor to break formation”.
If you want, I can convert the plate handling to a clean plate list (array) so other ships can spawn any number of plates without duplicating plateA/plateB logic, while still keeping it fast and simple.
Lets add: 2) Search pattern. 3) Plate break triggers a ram towards player until collision or missed. 4) “Sensor cone” counterplay with gas cloud.
Give me the full code for all events
Below is full code for all events for:
oArmorPlate (reusable armor plate object)oEnemyL1 (with FOV, search pattern, plate-break ram, gas cloud sensor cone counterplay, rockets + cannons + muzzle particles, rare strafing, collision avoid)It keeps:
closeDist = 800, shootDist = 1200image_angle, smoke side mirrored automaticallygmlevent_inherited(); parentShip = noone; // Configurable sprite_index = -1; image_speed = 0; maxHp = 20; hp = maxHp; plateArmor = 2; plateRicochetMult = 10;
gmlevent_inherited(); if (!instance_exists(parentShip)) { instance_destroy(); exit; } x = parentShip.x; y = parentShip.y; image_angle = parentShip.image_angle;
gmlevent_inherited(); if (hp <= 0) { part_particles_burst(global.partSystem, x, y, psShipExplode); }
gmlevent_inherited(); // Ensure shared particle types exist (best performance) if (!variable_global_exists("psL1CannonMuzzle")) { scL1CannonParticlesInit(); } // Core stats hp = real(80); infoName = "L1 'Hurricane'"; mass = 7000; threatAmount = 60; xpAmount = 150; circleRadius = 118; textOffset = -1280; baseArmor = 4; baseRicochetMult = 20; armor = baseArmor; ricochetMult = baseRicochetMult; sparksChance = 12; // Home / Patrol homeX = x; homeY = y; guardRadius = 7000; patrolTimer = 0; patrolIntervalMin = 120; patrolIntervalMax = 260; patrolTargetX = x; patrolTargetY = y; // Distances chaseDist = 4500; fireDist = 2600; closeDist = 800; // desired distance shootDist = 1200; // max cannon shooting distance // Movement maxSpeed = 8; slowdown = 0.25; seekAccel = 1.9; patrolAccel = 0.55; brakeAccel = 1.1; avoidLookAhead = 240; avoidStrength = 2.4; separationRadius = 200; separationStrength = 1.0; // Strafing (rare) strafeDir = choose(-1, 1); strafeActive = false; strafeActiveTimer = 0; strafeCooldownTimer = irandom_range(240, 420); strafeBurstMin = 22; strafeBurstMax = 45; strafeStrength = 1.05; strafeChance = 0.18; // Targeting target = noone; targetDist = 999999; checkForFriendly = true; // FOV + detection fovDist = 3800; fovHalfAngleBase = 55; fovHalfAngle = fovHalfAngleBase; playerSeen = false; playerLostTimerMax = 240; playerLostTimer = 0; lastKnownX = x; lastKnownY = y; // Searching pattern searchPhase = 0; // 0 = moving to lastKnown, 1 = sweeping pattern searchAngle = random(360); searchRadius = 240; searchRadiusMax = 1200; searchRadiusGrow = 2.4; searchAngleStep = 7; searchPointX = x; searchPointY = y; // Gas cloud counterplay inGasCloud = false; gasFovMult = 0.6; // sensor cone reduced rocketsDisabledInGas = true; // rockets off in gas cannonsOnlyInGas = true; // Rockets (barrage) rocketArmTime = 40; rocketVolleyDelay = 3; rocketReloadTime = 120; rocketMagazineReloadTime = 420; barrageShotsMax = 3; barrageShotsLeft = barrageShotsMax; rocketsPerSide = 6; rocketSpeed = 18; rocketSpread = 2; rocketOutShift = 10; rocketState = 0; // 0=ready, 1=arming, 2=volley, 3=reload, 4=magReload rocketTimer = 0; rocketVolleyTimer = 0; rocketVolleySide = 0; // 0=left, 1=right rocketVolleyRing = rocketsPerSide - 1; // inner -> outer dangerInst = noone; // Cannons cannonShotDelayBase = 4; cannonBurstReloadBase = 28; cannonShotDelay = cannonShotDelayBase; cannonBurstReload = cannonBurstReloadBase; cannonTimer = 0; cannonIndex = 0; // 0..5 L,R,L,R,L,R // Plate tracking platesAlive = 0; platesBroken = 0; platesBrokenPrev = 0; // Ram on plate break ramState = 0; // 0=off, 1=ramming, 2=cooldown ramTimer = 0; ramCooldownTimer = 0; ramSpeedMax = 13; ramAccel = 3.2; ramDuration = 90; ramCooldown = 210; ramDir = 0; ramStartDist = 0; ramMissFrames = 0; // Flares (kept) flareCooldown = 0; frMin = 5; frMax = 65; flareReactionTime = random_range(frMin, frMax); canDeployFlares = true; hasFlares = true; flaresAmount = 12; if (hasFlares) { flaresAmmo = 3; } else { flaresAmmo = 0; } // Visual sprite_index = texEnemyL1; image_speed = 0; // ------------------------ // Helper functions // ------------------------ getWorldFromSprite = function(_sx, _sy) { var dx = _sx - sprite_get_xoffset(texEnemyL1); var dy = _sy - sprite_get_yoffset(texEnemyL1); var wx = x + lengthdir_x(dx, image_angle) + lengthdir_x(dy, image_angle + 90); var wy = y + lengthdir_y(dx, image_angle) + lengthdir_y(dy, image_angle + 90); return [ wx, wy ]; }; pickNewPatrolPoint = function() { var ang = random(360); var rad = random_range(800, guardRadius); patrolTargetX = homeX + lengthdir_x(rad, ang); patrolTargetY = homeY + lengthdir_y(rad, ang); patrolTimer = irandom_range(patrolIntervalMin, patrolIntervalMax); }; getBarragePoint = function(_side, _ringIndex) { var t = 0; if (rocketsPerSide > 1) { t = _ringIndex / (rocketsPerSide - 1); } var sx1, sy1, sx2, sy2; var sideSign; if (_side == 0) { sx1 = 70; sy1 = 14; sx2 = 86; sy2 = 32; sideSign = -1; } else { sx1 = 70; sy1 = 114; sx2 = 86; sy2 = 96; sideSign = 1; } var sx = lerp(sx1, sx2, t); var sy = lerp(sy1, sy2, t); var p = getWorldFromSprite(sx, sy); var wx = p[0]; var wy = p[1]; var normalAng = image_angle + 90; wx += lengthdir_x(rocketOutShift * sideSign, normalAng); wy += lengthdir_y(rocketOutShift * sideSign, normalAng); return [ wx, wy ]; }; spawnRocketAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY) + random_range(-rocketSpread, rocketSpread); var r = instance_create_layer(_wx, _wy, "Instances", oRocket1); with (r) { hostile = true; direction = dir; image_angle = dir; speed = other.rocketSpeed; hspeed += other.hspeed; vspeed += other.vspeed; } }; spawnCannonAt = function(_wx, _wy, _aimX, _aimY) { var dir = point_direction(_wx, _wy, _aimX, _aimY); var b = instance_create_layer(_wx, _wy, "Instances", oCannonBullet); with (b) { hostile = true; direction = dir; image_angle = dir; speed = 55; hspeed += other.hspeed; vspeed += other.vspeed; } }; // Danger indicator (commented out) startArming = function() { rocketState = 1; rocketTimer = rocketArmTime; /* if (!instance_exists(dangerInst)) { dangerInst = instance_create_layer(x, y, "Instances", oEnemyL1Danger); with (dangerInst) { parentShip = other.id; } } */ }; startVolley = function() { rocketState = 2; rocketVolleyTimer = 0; rocketVolleySide = 0; rocketVolleyRing = rocketsPerSide - 1; /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; */ }; endBarrageAndReload = function() { barrageShotsLeft -= 1; if (barrageShotsLeft > 0) { rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketState = 4; rocketTimer = rocketMagazineReloadTime; } }; // ------------------------ // Spawn armor plates (2) // ------------------------ plateA = instance_create_layer(x, y, "Instances", oArmorPlate); with (plateA) { parentShip = other.id; sprite_index = texL1PlateA; image_angle = other.image_angle; maxHp = 28; hp = maxHp; plateArmor = 2; plateRicochetMult = 10; } platesAlive += 1; plateB = instance_create_layer(x, y, "Instances", oArmorPlate); with (plateB) { parentShip = other.id; sprite_index = texL1PlateB; image_angle = other.image_angle; maxHp = 28; hp = maxHp; plateArmor = 2; plateRicochetMult = 10; } platesAlive += 1;
gmlevent_inherited(); sprite_index = texEnemyL1; image_speed = 0; // Speed clamp + slowdown var currentMaxSpeed = maxSpeed; if (ramState == 1) { currentMaxSpeed = ramSpeedMax; } speed = clamp(speed, 0, currentMaxSpeed); if (speed > 0 && ramState != 1) { speed -= slowdown; if (speed < 0) { speed = 0; } } // Gas cloud detection (assumes object name oGasCloud) inGasCloud = place_meeting(x, y, oGasCloud); fovHalfAngle = fovHalfAngleBase; if (inGasCloud) { fovHalfAngle = fovHalfAngleBase * gasFovMult; } // Target acquire var playerExists = instance_exists(oPlayer) && global.playerExists; if (!playerExists) { playerSeen = false; playerLostTimer = 0; target = noone; } else { target = oPlayer; targetDist = point_distance(x, y, target.x, target.y); checkForFriendly = !collision_line(x, y, target.x, target.y, grpHostiles, true, true); var dirToPlayer = point_direction(x, y, target.x, target.y); var angDiff = abs(angle_difference(image_angle, dirToPlayer)); var inFov = (targetDist <= fovDist) && (angDiff <= fovHalfAngle); if (inFov) { playerSeen = true; playerLostTimer = playerLostTimerMax; lastKnownX = target.x; lastKnownY = target.y; searchPhase = 0; searchRadius = 240; searchAngle = point_direction(lastKnownX, lastKnownY, x, y); } else { if (playerSeen) { playerLostTimer -= 1; if (playerLostTimer <= 0) { playerSeen = false; target = noone; patrolTimer = 0; } } } } // Rare strafe logic (only while engaging/searching) if (playerSeen || playerLostTimer > 0) { if (!strafeActive) { strafeCooldownTimer -= 1; if (strafeCooldownTimer <= 0) { if (random(1) < strafeChance) { strafeActive = true; strafeDir = choose(-1, 1); strafeActiveTimer = irandom_range(strafeBurstMin, strafeBurstMax); } strafeCooldownTimer = irandom_range(240, 420); } } else { strafeActiveTimer -= 1; if (strafeActiveTimer <= 0) { strafeActive = false; } } } else { strafeActive = false; } // Separation from other hostiles (light) if (distance_to_object(grpHostiles) < separationRadius) { var inst = instance_place(x, y, grpHostiles); if (inst != noone) { var diffX = x - inst.x; var diffY = y - inst.y; var len = max(1, point_distance(0, 0, diffX, diffY)); diffX /= len; diffY /= len; hspeed += diffX * separationStrength; vspeed += diffY * separationStrength; } } // Collision avoidance vs grpCollide var avoidX = 0; var avoidY = 0; var lookX = x + lengthdir_x(avoidLookAhead, image_angle); var lookY = y + lengthdir_y(avoidLookAhead, image_angle); if (collision_line(x, y, lookX, lookY, grpCollide, true, true)) { avoidX += lengthdir_x(avoidStrength, image_angle + 90); avoidY += lengthdir_y(avoidStrength, image_angle + 90); } // Lead aim points var rocketAimX = 0; var rocketAimY = 0; var cannonAimX = 0; var cannonAimY = 0; if (target != noone) { var tR = targetDist / max(1, rocketSpeed); rocketAimX = target.x + (target.hspeed - hspeed) * tR; rocketAimY = target.y + (target.vspeed - vspeed) * tR; var cannonBulletSpeed = 55; var tC = targetDist / max(1, cannonBulletSpeed); cannonAimX = target.x + (target.hspeed - hspeed) * tC; cannonAimY = target.y + (target.vspeed - vspeed) * tC; var lookDir = point_direction(x, y, rocketAimX, rocketAimY); var rotFactor = -4.5; image_angle += sin(degtorad(image_angle - lookDir)) * rotFactor; } else { // Rotate toward patrol target when patrolling var pDir = point_direction(x, y, patrolTargetX, patrolTargetY); var pRot = -2.8; image_angle += sin(degtorad(image_angle - pDir)) * pRot; } // -------------------------------------------- // RAM BEHAVIOR (triggered in End Step) // -------------------------------------------- if (ramState == 2) { ramCooldownTimer -= 1; if (ramCooldownTimer <= 0) { ramState = 0; } } if (ramState == 1) { ramTimer -= 1; motion_add(ramDir, ramAccel); // Stop on collision with world if (place_meeting(x, y, grpCollide)) { ramState = 2; ramCooldownTimer = ramCooldown; } // Stop if we "missed" (distance growing for a bit) or timer ends if (target != noone) { var dNow = point_distance(x, y, target.x, target.y); if (dNow > ramStartDist + 160) { ramMissFrames += 1; } else { ramMissFrames = 0; } if (ramMissFrames >= 10) { ramState = 2; ramCooldownTimer = ramCooldown; } } if (ramTimer <= 0) { ramState = 2; ramCooldownTimer = ramCooldown; } // While ramming, skip normal movement/weapons hspeed += avoidX; vspeed += avoidY; // Flares still allowed } else { // -------------------------------------------- // NORMAL BEHAVIOR: Patrol / Search / Engage // -------------------------------------------- var patrolling = (!playerSeen && playerLostTimer <= 0); var searching = (!playerSeen && playerLostTimer > 0); var engaging = playerSeen; if (patrolling) { if (patrolTimer <= 0) { pickNewPatrolPoint(); } else { patrolTimer -= 1; } var pMoveDir = point_direction(x, y, patrolTargetX, patrolTargetY); motion_add(pMoveDir, patrolAccel); hspeed += avoidX; vspeed += avoidY; } else if (searching) { // Search pattern: // Phase 0: move to lastKnown // Phase 1: sweep around lastKnown with growing radius if (searchPhase == 0) { var dLast = point_distance(x, y, lastKnownX, lastKnownY); var sDir0 = point_direction(x, y, lastKnownX, lastKnownY); motion_add(sDir0, seekAccel * 0.85); if (dLast <= 220) { searchPhase = 1; searchAngle = point_direction(lastKnownX, lastKnownY, x, y); searchRadius = 240; } } else { searchAngle += searchAngleStep; searchRadius += searchRadiusGrow; if (searchRadius > searchRadiusMax) { searchRadius = 240; } searchPointX = lastKnownX + lengthdir_x(searchRadius, searchAngle); searchPointY = lastKnownY + lengthdir_y(searchRadius, searchAngle); var sDir1 = point_direction(x, y, searchPointX, searchPointY); motion_add(sDir1, seekAccel * 0.75); } hspeed += avoidX; vspeed += avoidY; } else if (engaging) { var toTargetDir = point_direction(x, y, target.x, target.y); var wantClose = (targetDist > closeDist + 120); var tooClose = (targetDist < closeDist - 160); if (wantClose) { motion_add(toTargetDir, seekAccel); } else if (tooClose) { motion_add(toTargetDir + 180, brakeAccel); } if (targetDist <= shootDist && strafeActive) { motion_add(toTargetDir + (90 * strafeDir), strafeStrength); } hspeed += avoidX; vspeed += avoidY; } // -------------------------------------------- // WEAPONS (only when engaging; optionally when searching too) // -------------------------------------------- var weaponsAllowed = playerSeen; // In gas: cannons only (optional setting) var rocketsAllowed = weaponsAllowed; if (inGasCloud && rocketsDisabledInGas) { rocketsAllowed = false; } // Rockets: only in band shootDist < dist <= fireDist var rocketOk = ( rocketsAllowed && target != noone && targetDist <= fireDist && targetDist > shootDist && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (rocketState == 0) { if (rocketOk) { startArming(); } } else if (rocketState == 1) { rocketTimer -= 1; if (!rocketOk) { rocketState = 0; rocketTimer = 0; /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } dangerInst = noone; */ } else if (rocketTimer <= 0) { startVolley(); } } else if (rocketState == 2) { if (!rocketOk) { rocketState = 3; rocketTimer = rocketReloadTime; } else { rocketVolleyTimer -= 1; if (rocketVolleyTimer <= 0) { if (rocketVolleyRing >= 0) { var p = getBarragePoint(rocketVolleySide, rocketVolleyRing); spawnRocketAt(p[0], p[1], rocketAimX, rocketAimY); if (rocketVolleySide == 0) { rocketVolleySide = 1; } else { rocketVolleySide = 0; rocketVolleyRing -= 1; } rocketVolleyTimer = rocketVolleyDelay; } else { endBarrageAndReload(); } } } } else if (rocketState == 3) { rocketTimer -= 1; if (rocketTimer <= 0) { rocketState = 0; rocketTimer = 0; } } else if (rocketState == 4) { rocketTimer -= 1; if (rocketTimer <= 0) { barrageShotsLeft = barrageShotsMax; rocketState = 0; rocketTimer = 0; } } // Cannons: dist <= shootDist var cannonOk = ( weaponsAllowed && target != noone && targetDist <= shootDist && checkForFriendly && !global.playerSafe && !oPlayer.trailActive ); if (inGasCloud && cannonsOnlyInGas) { // still ok (cannons only) } if (cannonOk) { if (cannonTimer <= 0) { if ((cannonIndex % 2) == 0) { var pL = getWorldFromSprite(107, 50); spawnCannonAt(pL[0], pL[1], cannonAimX, cannonAimY); scL1CannonMuzzleBurst(pL[0], pL[1], image_angle, false); } else { var pR = getWorldFromSprite(107, 78); spawnCannonAt(pR[0], pR[1], cannonAimX, cannonAimY); scL1CannonMuzzleBurst(pR[0], pR[1], image_angle, true); } cannonIndex += 1; if (cannonIndex >= 6) { cannonIndex = 0; cannonTimer = cannonBurstReload; } else { cannonTimer = cannonShotDelay; } } } if (cannonTimer > 0) { cannonTimer -= 1; } } // Flares (kept) if (hasFlares && flaresAmmo > 0 && canDeployFlares) { var shouldDeployFlares = false; with (oMissilePhase2) { var distToMissile = point_distance(other.x, other.y, x, y); if (distToMissile < 900) { shouldDeployFlares = true; break; } } if (shouldDeployFlares) { flareCooldown += 1; if (flareCooldown >= flareReactionTime) { DeployFlares(); flareCooldown = 0; flareReactionTime = random_range(frMin, frMax); } } else { flareCooldown = 0; } } // Danger indicator syncing (commented out) /* if (instance_exists(dangerInst)) { dangerInst.x = x; dangerInst.y = y; dangerInst.image_angle = image_angle; if (dangerInst.parentShip != id) { instance_destroy(dangerInst); dangerInst = noone; } } */
gmlevent_inherited(); // Aggregate plate stats var totalPlateArmor = 0; var totalPlateRicochet = 0; var alive = 0; if (instance_exists(plateA)) { totalPlateArmor += plateA.plateArmor; totalPlateRicochet += plateA.plateRicochetMult; alive += 1; } else { if (plateA != noone) { plateA = noone; platesBroken += 1; } } if (instance_exists(plateB)) { totalPlateArmor += plateB.plateArmor; totalPlateRicochet += plateB.plateRicochetMult; alive += 1; } else { if (plateB != noone) { plateB = noone; platesBroken += 1; } } platesAlive = alive; // Apply totals armor = baseArmor + totalPlateArmor; ricochetMult = baseRicochetMult + totalPlateRicochet; // Aggression tuning if (platesAlive <= 0) { cannonShotDelay = 3; cannonBurstReload = 22; rocketVolleyDelay = 2; } else { cannonShotDelay = cannonShotDelayBase; cannonBurstReload = cannonBurstReloadBase; rocketVolleyDelay = 3; } // Plate-break -> RAM trigger if (platesBroken > platesBrokenPrev) { platesBrokenPrev = platesBroken; // Start ram only if not already ramming/cooling down if (ramState == 0) { // We ram toward player if seen; otherwise toward last known if searching var canRam = (instance_exists(oPlayer) && global.playerExists); if (canRam) { var aimX = lastKnownX; var aimY = lastKnownY; if (playerSeen) { aimX = oPlayer.x; aimY = oPlayer.y; } ramDir = point_direction(x, y, aimX, aimY); ramTimer = ramDuration; ramState = 1; if (instance_exists(oPlayer)) { ramStartDist = point_distance(x, y, oPlayer.x, oPlayer.y); } else { ramStartDist = point_distance(x, y, aimX, aimY); } ramMissFrames = 0; // Optional: cancel weapons immediately rocketState = 0; rocketTimer = 0; cannonTimer = 0; cannonIndex = 0; } } }
gmlcanDeployFlares = true;
gmlevent_inherited(); /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } */
I used oGasCloud. If yours is different, replace it in Step:
gmlinGasCloud = place_meeting(x, y, oGasCloud);
I referenced:
texL1PlateAtexL1PlateBSwap to your real sprite names.
Now that L1 has plates + ram + FOV/search, it will feel very distinct. If you want to push it further later, good add-ons are:
If you want, I can also give you a generic “plate spawner” script so any ship can define an array of plate configs (sprite/hp/armor/ricochet) without duplicating plateA/plateB code.
if (!active)
{
return;
}
//draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle));
draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1);
if (instance_exists(grpHostiles) && !damagedFCS)
{
if (Wheel1)
{
HUD_Lead = true;
target = instance_nearest(mouse_x, mouse_y, grpHostiles);
}
textif (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { target = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { distToTarget = point_distance(x, y, target.x, target.y); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = target.x + (target.hspeed - hspeed) * Time; lead_y = target.y + (target.vspeed - vspeed) * Time; leadR_x = target.x + (target.hspeed - hspeed) * TimeRocket; leadR_y = target.y + (target.vspeed - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(target.x, target.y, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // Initialize armorColor to a default value var armorColor = c_white; var offset = 0; // Check if projectile exists and has penetration variable if projectile != noone { // Check if target has armor variable var targetArmor = 1; // Default armor if not defined if (variable_instance_exists(target, "armor")) { targetArmor = target.armor; } if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } // Calculate penetration ratio var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); // Cap for display // Determine armor color based on penetration ratio // Note: You have the colors inverted from what we discussed earlier // Here's your original inverted logic: if (_penetrationRatio >= 1.5) { // Very high penetration (≥1.5x) - penetration is 150%+ of armor // Shield is ineffective armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { // High penetration (1.0-1.5x) - penetration equals or exceeds armor // Shield is weak armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { // Medium-high penetration (0.75-1.0x) - penetration is 75%+ of armor // Shield is average armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { // Medium penetration (0.5-0.75x) - penetration is 50%+ of armor // Shield is effective armorColor = c_orange; } else { // Low penetration (<0.5x) - penetration is less than half of armor // Shield is very effective armorColor = c_red; } } // Draw Enemy Armor Icon draw_sprite_ext(texIconArmor, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(target.x-1, target.y + offset + 16, target.armor); }
}
Modify this code to also work with oArmorPlate
Below is a drop-in modification that keeps your logic intact, but makes the HUD armor display work for either:
target.armor)oArmorPlate.parentShip == target) if one existsIt also:
plateArmor if present (falls back to armor)circleRadius if it has one, otherwise uses the ship’s circleRadiusReplace this block:
gmlif (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { ... }
with the version below:
gmlif (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { distToTarget = point_distance(x, y, target.x, target.y); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = target.x + (target.hspeed - hspeed) * Time; lead_y = target.y + (target.vspeed - vspeed) * Time; leadR_x = target.x + (target.hspeed - hspeed) * TimeRocket; leadR_y = target.y + (target.vspeed - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(target.x, target.y, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // ------------------------------------------------------------ // Armor HUD (now supports oArmorPlate) // ------------------------------------------------------------ var armorColor = c_white; var offset = 0; // Determine whether we should show hull armor OR plate armor var armorTarget = target; // default = hull // Find closest armor plate belonging to this ship (if any) // NOTE: If you want to show "best" or "lowest hp" plate instead, change selection logic. var bestPlate = noone; var bestDist = 999999; with (oArmorPlate) { if (parentShip == other.target) { var d = point_distance(other.x, other.y, x, y); if (d < bestDist) { bestDist = d; bestPlate = id; } } } if (instance_exists(bestPlate)) { armorTarget = bestPlate; } // Pull armor value from armorTarget: // - armor plates use plateArmor // - ships use armor var targetArmor = 1; if (instance_exists(armorTarget)) { if (variable_instance_exists(armorTarget, "plateArmor")) { targetArmor = armorTarget.plateArmor; } else if (variable_instance_exists(armorTarget, "armor")) { targetArmor = armorTarget.armor; } } // Use offset from the armorTarget if it has circleRadius, otherwise from ship if (variable_instance_exists(armorTarget, "circleRadius")) { offset = armorTarget.circleRadius; } else if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } // Check penetration if projectile exists if (projectile != noone) { var _penetrationRatio = projectilePen / max(1, targetArmor); _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } // Draw Enemy Armor Icon + armor value // If armorTarget is a plate, icon will represent plate armor. draw_sprite_ext(texIconArmor, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); // Show the armor number of the thing we're actually evaluating (plate or hull) draw_text(target.x - 1, target.y + offset + 16, targetArmor); }
Since plates have hp, you can do:
gmlif (armorTarget != target && variable_instance_exists(armorTarget, "hp")) { draw_text(target.x - 1, target.y + offset + 30, string(armorTarget.hp)); }
This already happens naturally because bestPlate won’t exist → armorTarget = target.
Replace:
gmlvar d = point_distance(other.x, other.y, x, y);
with:
gmlvar d = point_distance(mouse_x, mouse_y, x, y);
If you want, I can also make the HUD icon change sprite when targeting a plate (e.g., texIconArmorPlate) so players instantly know whether they’re reading plate armor or hull armor.
I want it to show oArmorPlate.armor and oArmorPlate.hp
Perfect — below is a clean, minimal modification that makes the HUD explicitly show armor plate stats:
oArmorPlate.plateArmoroArmorPlate.hpI am not changing your structure, only extending it.
Replace only this part of your code:
gml// Initialize armorColor to a default value var armorColor = c_white; var offset = 0;
⬇️ with this full block ⬇️
gml// ------------------------------------------------------------ // ARMOR HUD (supports oArmorPlate with armor + hp) // ------------------------------------------------------------ var armorColor = c_white; var offset = 0; // Default to hull var armorTarget = target; // Find closest armor plate belonging to this ship var bestPlate = noone; var bestDist = 999999; with (oArmorPlate) { if (parentShip == other.target) { var d = point_distance(other.x, other.y, x, y); if (d < bestDist) { bestDist = d; bestPlate = id; } } } // If a plate exists, HUD shows plate stats instead of hull if (instance_exists(bestPlate)) { armorTarget = bestPlate; } // ------------------------------------------------------------ // Resolve armor + hp values // ------------------------------------------------------------ var targetArmor = 1; var targetHp = -1; // Armor plate case if (armorTarget != target && variable_instance_exists(armorTarget, "plateArmor")) { targetArmor = armorTarget.plateArmor; targetHp = armorTarget.hp; } else { // Hull fallback if (variable_instance_exists(target, "armor")) { targetArmor = target.armor; } } // Offset for HUD placement if (variable_instance_exists(armorTarget, "circleRadius")) { offset = armorTarget.circleRadius; } else if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } // ------------------------------------------------------------ // Penetration color logic // ------------------------------------------------------------ if (projectile != noone) { var _penetrationRatio = projectilePen / max(1, targetArmor); _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } // ------------------------------------------------------------ // Draw armor icon + values // ------------------------------------------------------------ draw_sprite_ext( texIconArmor, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1 ); draw_set_colour(c_black); // Draw armor value draw_text( target.x - 1, target.y + offset + 16, string(targetArmor) ); // Draw HP ONLY if this is an armor plate if (targetHp >= 0) { draw_text( target.x - 1, target.y + offset + 30, string(ceil(targetHp)) ); }
text[ ARMOR ICON ] 2 24 ← plate HP
[ ARMOR ICON ] 6
(HP line disappears automatically)
If you want instant readability:
gmlvar armorIcon = texIconArmor; if (armorTarget != target) { armorIcon = texIconArmorPlate; }
Then replace texIconArmor in draw_sprite_ext() with armorIcon.
oArmorPlate instancesIf you want next:
just say the word.
if (!active)
{
return;
}
//draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle));
draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1);
if (instance_exists(grpHostiles) && !damagedFCS)
{
if (Wheel1)
{
HUD_Lead = true;
target = instance_nearest(mouse_x, mouse_y, grpHostiles);
}
textif (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { target = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { distToTarget = point_distance(x, y, target.x, target.y); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = target.x + (target.hspeed - hspeed) * Time; lead_y = target.y + (target.vspeed - vspeed) * Time; leadR_x = target.x + (target.hspeed - hspeed) * TimeRocket; leadR_y = target.y + (target.vspeed - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(target.x, target.y, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // Initialize armorColor to a default value var armorColor = c_white; var offset = 0; // Check if projectile exists and has penetration variable if projectile != noone { // Check if target has armor variable var targetArmor = 1; // Default armor if not defined if (variable_instance_exists(target, "armor")) { targetArmor = target.armor; } if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } // Calculate penetration ratio var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); // Cap for display // Determine armor color based on penetration ratio // Note: You have the colors inverted from what we discussed earlier // Here's your original inverted logic: if (_penetrationRatio >= 1.5) { // Very high penetration (≥1.5x) - penetration is 150%+ of armor // Shield is ineffective armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { // High penetration (1.0-1.5x) - penetration equals or exceeds armor // Shield is weak armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { // Medium-high penetration (0.75-1.0x) - penetration is 75%+ of armor // Shield is average armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { // Medium penetration (0.5-0.75x) - penetration is 50%+ of armor // Shield is effective armorColor = c_orange; } else { // Low penetration (<0.5x) - penetration is less than half of armor // Shield is very effective armorColor = c_red; } } // Draw Enemy Armor Icon draw_sprite_ext(texIconArmor, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(target.x-1, target.y + offset + 16, target.armor); }
}
Add ability to select oArmorPlate the same way as grpHostiles. oArmorPlate should show its "armor" with texIconArmorPlate sprite, "hp" (no more "plateArmor", and "plateHP", i changed them to "armor" and "hp") and lead indicator
Here’s a drop-in modified version of your code that adds:
oArmorPlate the same way as grpHostiles (Wheel1 selects nearest to mouse among both)texIconArmorPlatetarget.armortarget.hptexIconArmor, target.armor)parentShip (falls back safely)Assumptions:
oArmorPlateexists and has vars:armor,hp, andparentShip(optional but recommended).- Plates sit at ship x/y so lead uses parent ship speed to be accurate.
gmlif (!active) { return; } //draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle)); draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1); if (instance_exists(grpHostiles) && !damagedFCS) { if (Wheel1) { HUD_Lead = true; // Select nearest among hostiles AND armor plates var _tHost = noone; var _tPlate = noone; if (instance_exists(grpHostiles)) { _tHost = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(oArmorPlate)) { _tPlate = instance_nearest(mouse_x, mouse_y, oArmorPlate); } if (instance_exists(_tHost) && instance_exists(_tPlate)) { var _dH = point_distance(mouse_x, mouse_y, _tHost.x, _tHost.y); var _dP = point_distance(mouse_x, mouse_y, _tPlate.x, _tPlate.y); target = (_dP < _dH) ? _tPlate : _tHost; } else if (instance_exists(_tPlate)) { target = _tPlate; } else { target = _tHost; } } if (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { // Reacquire using same dual-selection var _tHost2 = noone; var _tPlate2 = noone; if (instance_exists(grpHostiles)) { _tHost2 = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(oArmorPlate)) { _tPlate2 = instance_nearest(mouse_x, mouse_y, oArmorPlate); } if (instance_exists(_tHost2) && instance_exists(_tPlate2)) { var _dH2 = point_distance(mouse_x, mouse_y, _tHost2.x, _tHost2.y); var _dP2 = point_distance(mouse_x, mouse_y, _tPlate2.x, _tPlate2.y); target = (_dP2 < _dH2) ? _tPlate2 : _tHost2; } else if (instance_exists(_tPlate2)) { target = _tPlate2; } else { target = _tHost2; } } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { // ------------------------------------------------------------ // Lead indicator (works for hull OR plate) // If plate has parentShip, use its velocity for accurate lead. // ------------------------------------------------------------ var _tX = target.x; var _tY = target.y; var _tH = target.hspeed; var _tV = target.vspeed; var _isPlate = (target.object_index == oArmorPlate); if (_isPlate) { if (variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { _tH = target.parentShip.hspeed; _tV = target.parentShip.vspeed; // If you ever offset plate position later, still use plate x/y for hit point. } } distToTarget = point_distance(x, y, _tX, _tY); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = _tX + (_tH - hspeed) * Time; lead_y = _tY + (_tV - vspeed) * Time; leadR_x = _tX + (_tH - hspeed) * TimeRocket; leadR_y = _tY + (_tV - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(_tX, _tY, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // ------------------------------------------------------------ // Armor + HP HUD (plate shows armor + hp, hull shows armor only) // ------------------------------------------------------------ var armorColor = c_white; var offset = 0; // Read armor from target (plate or hull) var targetArmor = 1; if (variable_instance_exists(target, "armor")) { targetArmor = max(1, target.armor); } // Offset: prefer hull circleRadius if plate has parentShip if (_isPlate && variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { if (variable_instance_exists(target.parentShip, "circleRadius")) { offset = target.parentShip.circleRadius; } } else { if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } } // Penetration color logic if (projectile != noone) { var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } // Choose correct icon var _icon = texIconArmor; if (_isPlate) { _icon = texIconArmorPlate; } // Draw icon at hull position (target.x/target.y are same for plates per your design) draw_sprite_ext(_icon, 0, _tX, _tY + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); // Armor number draw_text(_tX - 1, _tY + offset + 16, string(targetArmor)); // Plate HP if (_isPlate && variable_instance_exists(target, "hp")) { draw_text(_tX - 1, _tY + offset + 30, string(ceil(target.hp))); } } }
Right now it selects any plate nearest the mouse. If you later want “only plates that belong to hovered ship”, tell me and I’ll adjust selection to:
parentShip == that shipBut as requested, this version treats plates as first-class selectable targets.
yes, please
Got it — here’s the upgraded version where selection works like this:
grpHostiles) near the mouseparentShip)Drop-in replacement for your whole block:
gmlif (!active) { return; } //draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle)); draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1); if (instance_exists(grpHostiles) && !damagedFCS) { if (Wheel1) { HUD_Lead = true; // 1) Pick nearest hostile ship var _ship = instance_nearest(mouse_x, mouse_y, grpHostiles); // 2) Try to pick nearest plate that belongs to that ship var _plate = noone; var _plateDist = 999999; if (instance_exists(_ship) && instance_exists(oArmorPlate)) { with (oArmorPlate) { if (parentShip == other._ship) { var d = point_distance(mouse_x, mouse_y, x, y); if (d < other._plateDist) { other._plateDist = d; other._plate = id; } } } } // 3) Choose plate only if it's closer to mouse than ship is if (instance_exists(_ship)) { var _shipDist = point_distance(mouse_x, mouse_y, _ship.x, _ship.y); if (instance_exists(_plate) && _plateDist < _shipDist) { target = _plate; } else { target = _ship; } } else { target = noone; } } if (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { // Reacquire using same ship->plate logic var _ship2 = instance_nearest(mouse_x, mouse_y, grpHostiles); var _plate2 = noone; var _plateDist2 = 999999; if (instance_exists(_ship2) && instance_exists(oArmorPlate)) { with (oArmorPlate) { if (parentShip == other._ship2) { var d2 = point_distance(mouse_x, mouse_y, x, y); if (d2 < other._plateDist2) { other._plateDist2 = d2; other._plate2 = id; } } } } if (instance_exists(_ship2)) { var _shipDist2 = point_distance(mouse_x, mouse_y, _ship2.x, _ship2.y); if (instance_exists(_plate2) && _plateDist2 < _shipDist2) { target = _plate2; } else { target = _ship2; } } else { target = noone; } } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { // ------------------------------------------------------------ // Lead indicator (works for hull OR plate) // Plate lead uses parentShip velocity if available. // ------------------------------------------------------------ var _tX = target.x; var _tY = target.y; var _tH = target.hspeed; var _tV = target.vspeed; var _isPlate = (target.object_index == oArmorPlate); if (_isPlate) { if (variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { _tH = target.parentShip.hspeed; _tV = target.parentShip.vspeed; } } distToTarget = point_distance(x, y, _tX, _tY); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = _tX + (_tH - hspeed) * Time; lead_y = _tY + (_tV - vspeed) * Time; leadR_x = _tX + (_tH - hspeed) * TimeRocket; leadR_y = _tY + (_tV - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(_tX, _tY, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // ------------------------------------------------------------ // Armor + HP HUD // Plate: texIconArmorPlate + armor + hp // Hull: texIconArmor + armor // ------------------------------------------------------------ var armorColor = c_white; var offset = 0; var targetArmor = 1; if (variable_instance_exists(target, "armor")) { targetArmor = max(1, target.armor); } // Offset: if plate, prefer parent's circleRadius; else target's if (_isPlate && variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { if (variable_instance_exists(target.parentShip, "circleRadius")) { offset = target.parentShip.circleRadius; } } else { if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } } // Penetration color logic if (projectile != noone) { var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } var _icon = texIconArmor; if (_isPlate) { _icon = texIconArmorPlate; } draw_sprite_ext(_icon, 0, _tX, _tY + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(_tX - 1, _tY + offset + 16, string(targetArmor)); if (_isPlate && variable_instance_exists(target, "hp")) { draw_text(_tX - 1, _tY + offset + 30, string(ceil(target.hp))); } } }
If you want the plate to be selectable only when the mouse is within the plate’s collision mask (instead of distance), tell me what collision check you prefer (position_meeting, collision_point, etc.) and I’ll swap the “distance compare” to a true “hover hit test.”
ERROR!!! :: ############################################################################################
ERROR in action number 1
of Draw Event for object oPlayer:
Variable oPlayer._ship(103582, -2147483648) not set before reading it.
at gml_Object_oPlayer_Draw_73 (line 27) - if (parentShip == other._ship)
############################################################################################
gml_Object_oPlayer_Draw_73 (line 27)
That error is because with (oArmorPlate) { ... other._ship ... } is reading other._ship, but inside a with block other is not reliably your oPlayer, so _ship doesn’t exist there.
Fix: don’t use other._ship inside with. Store the ship id in a normal variable and compare against it directly.
Here’s a corrected, safe version (drop-in replacement for your whole block). It also handles cases where the plate doesn’t have parentShip yet.
gmlif (!active) { return; } //draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle)); draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1); if (instance_exists(grpHostiles) && !damagedFCS) { if (Wheel1) { HUD_Lead = true; // 1) Pick nearest hostile ship var ship = instance_nearest(mouse_x, mouse_y, grpHostiles); // 2) Find nearest plate belonging to that ship (by parentShip) var plate = noone; var plateDist = 1e30; if (instance_exists(ship) && instance_exists(oArmorPlate)) { var shipId = ship.id; // iterate plates safely without using other._ship var p = instance_first(oArmorPlate); while (p != noone) { if (variable_instance_exists(p, "parentShip") && p.parentShip == shipId) { var d = point_distance(mouse_x, mouse_y, p.x, p.y); if (d < plateDist) { plateDist = d; plate = p; } } p = instance_next(p); } } // 3) Choose plate only if it's closer to mouse than ship is if (instance_exists(ship)) { var shipDist = point_distance(mouse_x, mouse_y, ship.x, ship.y); if (instance_exists(plate) && plateDist < shipDist) { target = plate; } else { target = ship; } } else { target = noone; } } if (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { // Reacquire using same ship->plate logic var ship2 = instance_nearest(mouse_x, mouse_y, grpHostiles); var plate2 = noone; var plateDist2 = 1e30; if (instance_exists(ship2) && instance_exists(oArmorPlate)) { var shipId2 = ship2.id; var p2 = instance_first(oArmorPlate); while (p2 != noone) { if (variable_instance_exists(p2, "parentShip") && p2.parentShip == shipId2) { var d2 = point_distance(mouse_x, mouse_y, p2.x, p2.y); if (d2 < plateDist2) { plateDist2 = d2; plate2 = p2; } } p2 = instance_next(p2); } } if (instance_exists(ship2)) { var shipDist2 = point_distance(mouse_x, mouse_y, ship2.x, ship2.y); if (instance_exists(plate2) && plateDist2 < shipDist2) { target = plate2; } else { target = ship2; } } else { target = noone; } } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { // ------------------------------------------------------------ // Lead indicator (works for hull OR plate) // Plate lead uses parentShip velocity if available // ------------------------------------------------------------ var tX = target.x; var tY = target.y; var tH = target.hspeed; var tV = target.vspeed; var isPlate = (target.object_index == oArmorPlate); if (isPlate) { if (variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { tH = target.parentShip.hspeed; tV = target.parentShip.vspeed; } } distToTarget = point_distance(x, y, tX, tY); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = tX + (tH - hspeed) * Time; lead_y = tY + (tV - vspeed) * Time; leadR_x = tX + (tH - hspeed) * TimeRocket; leadR_y = tY + (tV - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(tX, tY, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // ------------------------------------------------------------ // Armor + HP HUD // Plate: texIconArmorPlate + armor + hp // Hull: texIconArmor + armor // ------------------------------------------------------------ var armorColor = c_white; var offset = 0; var targetArmor = 1; if (variable_instance_exists(target, "armor")) { targetArmor = max(1, target.armor); } if (isPlate && variable_instance_exists(target, "parentShip") && instance_exists(target.parentShip)) { if (variable_instance_exists(target.parentShip, "circleRadius")) { offset = target.parentShip.circleRadius; } } else if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } if (projectile != noone) { var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) armorColor = c_aqua; else if (_penetrationRatio >= 1.0) armorColor = c_lime; else if (_penetrationRatio >= 0.75) armorColor = c_yellow; else if (_penetrationRatio >= 0.5) armorColor = c_orange; else armorColor = c_red; } var icon = texIconArmor; if (isPlate) { icon = texIconArmorPlate; } draw_sprite_ext(icon, 0, tX, tY + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(tX - 1, tY + offset + 16, string(targetArmor)); if (isPlate && variable_instance_exists(target, "hp")) { draw_text(tX - 1, tY + offset + 30, string(ceil(target.hp))); } } }
If you want the selection to prefer plates only when the mouse is actually over the plate’s collision mask, I can swap the “distance compare” with collision_point(mouse_x, mouse_y, oArmorPlate, true, false) filtering during the plate scan.
lets make things simpler, just choose between grpHostiles and oArmorPlate
if (!active)
{
return;
}
//draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle));
draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1);
if (instance_exists(grpHostiles) && !damagedFCS)
{
if (Wheel1)
{
HUD_Lead = true;
target = instance_nearest(mouse_x, mouse_y, grpHostiles);
}
textif (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { target = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { distToTarget = point_distance(x, y, target.x, target.y); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = target.x + (target.hspeed - hspeed) * Time; lead_y = target.y + (target.vspeed - vspeed) * Time; leadR_x = target.x + (target.hspeed - hspeed) * TimeRocket; leadR_y = target.y + (target.vspeed - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(target.x, target.y, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // Initialize armorColor to a default value var armorColor = c_white; var offset = 0; // Check if projectile exists and has penetration variable if projectile != noone { // Check if target has armor variable var targetArmor = 1; // Default armor if not defined if (variable_instance_exists(target, "armor")) { targetArmor = target.armor; } if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } // Calculate penetration ratio var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); // Cap for display // Determine armor color based on penetration ratio // Note: You have the colors inverted from what we discussed earlier // Here's your original inverted logic: if (_penetrationRatio >= 1.5) { // Very high penetration (≥1.5x) - penetration is 150%+ of armor // Shield is ineffective armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { // High penetration (1.0-1.5x) - penetration equals or exceeds armor // Shield is weak armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { // Medium-high penetration (0.75-1.0x) - penetration is 75%+ of armor // Shield is average armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { // Medium penetration (0.5-0.75x) - penetration is 50%+ of armor // Shield is effective armorColor = c_orange; } else { // Low penetration (<0.5x) - penetration is less than half of armor // Shield is very effective armorColor = c_red; } } // Draw Enemy Armor Icon draw_sprite_ext(texIconArmor, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(target.x-1, target.y + offset + 16, target.armor); }
}
Here’s the simplified version that just chooses between grpHostiles and oArmorPlate (whichever is closer to the mouse), and then:
texIconArmorPlate for plates, texIconArmor for shipsgmlif (!active) { return; } //draw_line(x, y, x+lengthdir_x(2000, image_angle), y+lengthdir_y(2000, image_angle)); draw_sprite_ext(texPlayerID, lobbyMemberID, x, y, size, size, image_angle, c_white, 1); if (!damagedFCS) { if (Wheel1) { HUD_Lead = true; // Choose nearest between grpHostiles and oArmorPlate var tHost = noone; var tPlate = noone; if (instance_exists(grpHostiles)) { tHost = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(oArmorPlate)) { tPlate = instance_nearest(mouse_x, mouse_y, oArmorPlate); } if (instance_exists(tHost) && instance_exists(tPlate)) { var dH = point_distance(mouse_x, mouse_y, tHost.x, tHost.y); var dP = point_distance(mouse_x, mouse_y, tPlate.x, tPlate.y); target = (dP < dH) ? tPlate : tHost; } else if (instance_exists(tPlate)) { target = tPlate; } else { target = tHost; } } if (WheelX2) { HUD_Lead = false; target = noone; } if (HUD_Lead && !instance_exists(target)) { // Reacquire using same nearest-between-two logic var tHost2 = noone; var tPlate2 = noone; if (instance_exists(grpHostiles)) { tHost2 = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(oArmorPlate)) { tPlate2 = instance_nearest(mouse_x, mouse_y, oArmorPlate); } if (instance_exists(tHost2) && instance_exists(tPlate2)) { var dH2 = point_distance(mouse_x, mouse_y, tHost2.x, tHost2.y); var dP2 = point_distance(mouse_x, mouse_y, tPlate2.x, tPlate2.y); target = (dP2 < dH2) ? tPlate2 : tHost2; } else if (instance_exists(tPlate2)) { target = tPlate2; } else { target = tHost2; } } if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { // Lead indicator distToTarget = point_distance(x, y, target.x, target.y); bulletSpeed = projectileSpeed; rocketSpeed = global.playerRocketSpeed1; var Time = distToTarget * 1.2 / bulletSpeed; var TimeRocket = distToTarget * 1.4 / rocketSpeed; lead_x = target.x + (target.hspeed - hspeed) * Time; lead_y = target.y + (target.vspeed - vspeed) * Time; leadR_x = target.x + (target.hspeed - hspeed) * TimeRocket; leadR_y = target.y + (target.vspeed - vspeed) * TimeRocket; draw_set_colour(c_lime); draw_set_alpha(0.2); draw_line(target.x, target.y, lead_x, lead_y); draw_set_alpha(1); draw_ellipse(lead_x - 8, lead_y - 8, lead_x + 8, lead_y + 8, true); draw_rectangle(leadR_x - 8, leadR_y - 8, leadR_x + 8, leadR_y + 8, true); // Armor HUD var armorColor = c_white; var offset = 0; var targetArmor = 1; if (variable_instance_exists(target, "armor")) { targetArmor = max(1, target.armor); } if (variable_instance_exists(target, "circleRadius")) { offset = target.circleRadius; } if (projectile != noone) { var _penetrationRatio = projectilePen / targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } var isPlate = (target.object_index == oArmorPlate); var icon = texIconArmor; if (isPlate) { icon = texIconArmorPlate; } // Draw icon + armor value draw_sprite_ext(icon, 0, target.x, target.y + offset + 16, 1, 1, 0, armorColor, 1); draw_set_colour(c_black); draw_text(target.x - 1, target.y + offset + 16, string(targetArmor)); // Draw HP only for plates if (isPlate && variable_instance_exists(target, "hp")) { draw_text(target.x - 1, target.y + offset + 30, string(ceil(target.hp))); } } }
If you want one extra simplification: we can remove the reacquire block entirely and just re-run the “nearest-between-two” selection every frame while HUD_Lead is true (so it auto-swaps targets as you move your mouse).
It selects only the ship
That’s happening because your plates sit at the same x/y as the ship, so dP and dH are usually equal — and your code uses (dP < dH), which breaks ties in favor of the ship.
Make plates win ties (or “almost ties”), and it’ll select plates correctly.
Here’s the simplest fix:
dP <= dH (tie goes to plate)Use this block in both places (Wheel1 and reacquire):
gml// Choose nearest between grpHostiles and oArmorPlate var tHost = noone; var tPlate = noone; if (instance_exists(grpHostiles)) { tHost = instance_nearest(mouse_x, mouse_y, grpHostiles); } if (instance_exists(oArmorPlate)) { tPlate = instance_nearest(mouse_x, mouse_y, oArmorPlate); } if (instance_exists(tHost) && instance_exists(tPlate)) { var dH = point_distance(mouse_x, mouse_y, tHost.x, tHost.y); var dP = point_distance(mouse_x, mouse_y, tPlate.x, tPlate.y); // Bias so plates win ties / near-ties (since they share x/y with ships) var plateBias = 6; // pixels; tweak 0..20 target = (dP <= dH + plateBias) ? tPlate : tHost; } else if (instance_exists(tPlate)) { target = tPlate; } else { target = tHost; }
If your plates have proper collision masks and you only want to select a plate when the mouse is actually over it, do this instead:
gmlvar tHost = noone; var tPlate = noone; if (instance_exists(grpHostiles)) { tHost = instance_nearest(mouse_x, mouse_y, grpHostiles); } // plate must be under mouse tPlate = instance_position(mouse_x, mouse_y, oArmorPlate); if (instance_exists(tPlate)) { target = tPlate; } else { target = tHost; }
If you tell me whether you want “plate wins always near ship” (first fix) or “only when mouse is over plate mask” (second fix), I’ll merge it into your full draw block cleanly.
Please modify these scripts
If a bullet ricochets, it cant collide with the object it ricocheted with
Simple ricochet angle = bullet rear semicircle (towards direction the bullet came from)
If bullet pen is 3 levels below target armor = no damage
Blastwave damage depends on targets armor
function ApplyPlayerDamage(_damage, _penetration, _damageModule = 10,
_hitSound = noone, _hitParticle = noone, _ricochetParticle = noone, _ricochetSound = noone,
_screenshake = 0, _screenshakeTime = 0, _ricochetChance = 20, _bulletInstance = self, _destroy = true)
{
textif (!instance_exists(oPlayer)) { exit; } // Get player armor var _targetArmor = 1; if (variable_instance_exists(oPlayer, "armor")) { _targetArmor = oPlayer.armor; } // Calculate penetration ratio with capping var _penetrationRatio = _penetration / _targetArmor; // Apply damage cap: minimum 0.25x, maximum 1.75x _penetrationRatio = clamp(_penetrationRatio, 0.25, 1.75); // Calculate effective damage var _effectiveDamage = _damage * _penetrationRatio; _effectiveDamage = max(0.1, _effectiveDamage); // Ensure at least 0.1 damage // Calculate ricochet chance //var _ricochetChance = max(0, (_targetArmor - _penetration) * _ricochetMultiplier); // Check for ricochet if (irandom(100) < _ricochetChance) { // Ricochet occurred if (instance_exists(_bulletInstance)) { // Get the angle of the surface being hit (target's facing) var surfaceAngle = other.image_angle; // Get bullet's current direction var bulletAngle = _bulletInstance.direction; // Calculate ricochet angle: 2 * surfaceAngle - bulletAngle var ricochetAngle = 2 * surfaceAngle - bulletAngle; // Normalize to 0-360 range ricochetAngle = (ricochetAngle + 360) mod 360; // Apply ricochet angle with less randomness var randomSpread = random_range(-45, 45); _bulletInstance.direction = ricochetAngle + randomSpread; _bulletInstance.image_angle = _bulletInstance.direction; // Keep your original speed reduction _bulletInstance.speed *= random_range(0.3, 0.8); _bulletInstance.flicker = true; } // Keep your original effects if (_ricochetParticle != noone) { part_particles_burst(global.partSystem, _bulletInstance.x, _bulletInstance.y, _ricochetParticle); } if (_ricochetSound != noone) { oSoundManager.playSoundAt(_ricochetSound, _bulletInstance.x, _bulletInstance.y, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 1, 1, random_range(0.8, 1.2)); } // Keep your original damage calculation oPlayer.playerHealth -= max(0.01, (_effectiveDamage * 0.1)); return { damageDealt: 0, ricochet: true, effectiveDamage: 0 }; } else { // Apply damage to player oPlayer.playerHealth -= _effectiveDamage; } // moduleDamage = percentage chance (0-100) if (irandom(100) < _damageModule) { // Damage a random module (1-8) oPlayer.damagedModuleChance = irandom_range(1, 8); } global.playerTookDamage = true; // Apply screenshake if (_screenshake > 0) { global.screenshake = _screenshake; global.screenshake_time = _screenshakeTime; } // Hit effects var _effectX = x; var _effectY = y; if (instance_exists(_bulletInstance)) { _effectX = _bulletInstance.x; _effectY = _bulletInstance.y; } if (_hitParticle != noone) { part_particles_burst(global.partSystem, _effectX, _effectY, _hitParticle); } if (_hitSound != noone) { oSoundManager.playSoundAt(_hitSound, _effectX, _effectY, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 2, 1, random_range(0.8, 1.2)); } // Debug info show_debug_message("Player Damage: Base=" + string(_damage) + ", Pen=" + string(_penetration) + ", Armor=" + string(_targetArmor) + ", Ratio=" + string(_penetrationRatio) + ", Final=" + string(_effectiveDamage)); // Destroy bullet if (instance_exists(_bulletInstance) && _destroy) { instance_destroy(_bulletInstance); } return { damageDealt: _effectiveDamage, ricochet: false, effectiveDamage: _effectiveDamage, penetrationRatio: _penetrationRatio };
}
function ApplyDamage(_target, _damage, _penetration, _damageModule = 10,
_hitSound = noone, _hitParticle = noone, _ricochetParticle = noone, _ricochetSound = noone,
_ricochetChance = 20, _bulletInstance = self, _destroy = true)
{
if (!instance_exists(_target))
{
exit;
}
text// Get target armor var _targetArmor = 1; if (variable_instance_exists(_target, "armor")) { _targetArmor = _target.armor; } // Calculate penetration ratio with capping var _penetrationRatio = _penetration / _targetArmor; // Apply damage cap: minimum 0.25x, maximum 1.75x _penetrationRatio = clamp(_penetrationRatio, 0.25, 1.75); // Calculate effective damage var _effectiveDamage = _damage * _penetrationRatio; _effectiveDamage = max(0.01, _effectiveDamage); // Ensure at least 1 damage // Calculate ricochet chance //var _ricochetChance = max(0, (_targetArmor - _penetration) * _ricochetMultiplier); // Check for ricochet if (irandom(100) < _ricochetChance) { // Ricochet occurred if (instance_exists(_bulletInstance)) { show_debug_message("RICOCHET!"); // Get the angle of the surface being hit (target's facing) var surfaceAngle = _target.image_angle; // Get bullet's current direction var bulletAngle = _bulletInstance.direction; // Calculate ricochet angle: 2 * surfaceAngle - bulletAngle var ricochetAngle = 2 * surfaceAngle - bulletAngle; // Normalize to 0-360 range ricochetAngle = (ricochetAngle + 360) mod 360; // Apply ricochet angle with less randomness var randomSpread = random_range(105, 255); _bulletInstance.direction = ricochetAngle + randomSpread; _bulletInstance.image_angle = _bulletInstance.direction; // Keep your original speed reduction _bulletInstance.speed *= random_range(0.3, 0.8); _bulletInstance.flicker = true; } // Ricochet effects if (_ricochetParticle != noone) { part_particles_burst(global.partSystem, _bulletInstance.x, _bulletInstance.y, _ricochetParticle); } if (_ricochetSound != noone) { oSoundManager.playSoundAt(_ricochetSound, _bulletInstance.x, _bulletInstance.y, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 1, 1, random_range(0.8, 1.2)); } // Apply damage to enemy _target.hp -= max(0.01, (_effectiveDamage * 0.1)); return { damageDealt: 0, ricochet: true, effectiveDamage: 0 }; } else { show_debug_message("HIT!"); // Apply damage to enemy _target.hp -= _effectiveDamage; } // Additional effects if (variable_instance_exists(_target, "bulletFlinch")) { _target.image_angle += irandom_range(-_target.bulletFlinch, _target.bulletFlinch); } // Set damagedByPlayer flag if (variable_instance_exists(_bulletInstance, "byPlayer") && _bulletInstance.byPlayer = true) { if (variable_instance_exists(_target, "damagedByPlayer")) { _target.damagedByPlayer = true; } } // Hit effects var _effectX = x; var _effectY = y; if (instance_exists(_bulletInstance)) { _effectX = _bulletInstance.x; _effectY = _bulletInstance.y; } if (_hitParticle != noone) { part_particles_burst(global.partSystem, _effectX, _effectY, _hitParticle); } if (_hitSound != noone) { oSoundManager.playSoundAt(_hitSound, _effectX, _effectY, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 2, 1, random_range(0.8, 1.2)); } // Debug info show_debug_message("NPC Damage to " + (variable_instance_exists(_target, "infoName") ? _target.infoName : object_get_name(_target.object_index)) + ": Base =" + string(_damage) + ", Pen =" + string(_penetration) + ", Armor =" + string(_targetArmor) + ", Ratio =" + string(_penetrationRatio) + ", Final =" + string(_effectiveDamage)); // Destroy bullet if (instance_exists(_bulletInstance) && _destroy) { instance_destroy(_bulletInstance); } return { damageDealt: _effectiveDamage, ricochet: false, effectiveDamage: _effectiveDamage, penetrationRatio: _penetrationRatio };
}
function Blastwave(blastwaveRadius, blastwaveStrength, damageNPC, damagePlayer, shakeTime, shakeStrenght, byPlayer, moduleDamage)
{
with (grpPhysics)
{
var dist = point_distance(other.x, other.y, x, y);
if (dist < blastwaveRadius)
{
textif (!variable_instance_exists(other, "armor")) { other.armor = 1; } var angleToBlastwave = point_direction(other.x, other.y, x, y); var pushStrength = (blastwaveRadius - dist) / blastwaveRadius * blastwaveStrength / other.mass; // Force based only on distance var damageStrenght = ((blastwaveRadius - dist) / blastwaveRadius * blastwaveStrength / 100) / other.armor; var pushX = lengthdir_x(pushStrength, angleToBlastwave); var pushY = lengthdir_y(pushStrength, angleToBlastwave); if (object_index == oPlayer) { var playerDamage = round(damageStrenght * damagePlayer); playerHealth -= playerDamage; //global.threatPoints += playerDamage; show_debug_message("Blastdamage to Player: " + string(playerDamage)); show_debug_message("Player Health: " + string(playerHealth)); global.screenshake = pushStrength * shakeStrenght; global.screenshake_time = shakeTime; global.playerTookDamage = true; // moduleDamage = percentage chance (0-100) if (irandom(100) < moduleDamage) { // Damage a random module (1-8) damagedModuleChance = irandom_range(0, 8); } } if byPlayer { with (grpHostiles) { damagedByPlayer = true; } } var npcDamage = round(damageStrenght * damageNPC); hp -= npcDamage; show_debug_message((infoName != undefined ? infoName : object_get_name(object_index)) + " received blastdamage: " + string(npcDamage)); show_debug_message("NPC Health: " + string(hp)); if object_index != oM22C { motion_add(angleToBlastwave, pushStrength); } } }
}
Below are modified versions of your three scripts implementing exactly what you asked:
✅ If a bullet ricochets, it can’t collide with the object it ricocheted off (sets ignore vars on the bullet)
✅ Simple ricochet angle = rear semicircle (bullet bounces back roughly where it came from)
✅ If bullet pen is 3 levels below target armor → NO DAMAGE
✅ Blastwave damage depends on target’s armor (clean + safe, always divides by armor)
gmlfunction ApplyPlayerDamage(_damage, _penetration, _damageModule = 10, _hitSound = noone, _hitParticle = noone, _ricochetParticle = noone, _ricochetSound = noone, _screenshake = 0, _screenshakeTime = 0, _ricochetChance = 20, _bulletInstance = self, _destroy = true) { if (!instance_exists(oPlayer)) { exit; } // Get player armor var _targetArmor = 1; if (variable_instance_exists(oPlayer, "armor")) { _targetArmor = max(1, oPlayer.armor); } // If penetration is 3 levels below armor -> no damage var _noDamage = (_penetration <= (_targetArmor - 3)); // Calculate penetration ratio (only matters if we can damage) var _penetrationRatio = 0; var _effectiveDamage = 0; if (!_noDamage) { _penetrationRatio = _penetration / _targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0.25, 1.75); _effectiveDamage = _damage * _penetrationRatio; _effectiveDamage = max(0.1, _effectiveDamage); } // ------------------------------------------------------------ // Ricochet // Simple ricochet angle: rear semicircle (towards where bullet came from) // Bullet cannot collide with the same object it ricocheted with // ------------------------------------------------------------ if (irandom(100) < _ricochetChance) { if (instance_exists(_bulletInstance)) { var bulletAngle = _bulletInstance.direction; // Rear semicircle: center at bulletAngle+180, spread +/- 90 var ricochetAngle = bulletAngle + 180 + random_range(-90, 90); ricochetAngle = (ricochetAngle + 360) mod 360; _bulletInstance.direction = ricochetAngle; _bulletInstance.image_angle = _bulletInstance.direction; _bulletInstance.speed *= random_range(0.3, 0.8); _bulletInstance.flicker = true; // IGNORE the object we ricocheted off (player) _bulletInstance.ricochetIgnoreId = oPlayer.id; _bulletInstance.ricochetIgnoreTime = 12; // frames; tune as needed } if (_ricochetParticle != noone && instance_exists(_bulletInstance)) { part_particles_burst(global.partSystem, _bulletInstance.x, _bulletInstance.y, _ricochetParticle); } if (_ricochetSound != noone && instance_exists(_bulletInstance)) { oSoundManager.playSoundAt(_ricochetSound, _bulletInstance.x, _bulletInstance.y, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 1, 1, random_range(0.8, 1.2)); } // Ricochet "chip" damage only if it wasn't a total no-damage case if (!_noDamage) { oPlayer.playerHealth -= max(0.01, (_effectiveDamage * 0.1)); } return { damageDealt: 0, ricochet: true, effectiveDamage: 0 }; } // ------------------------------------------------------------ // Normal hit (no ricochet) // ------------------------------------------------------------ if (!_noDamage) { oPlayer.playerHealth -= _effectiveDamage; } // else: no damage at all // moduleDamage = percentage chance (0-100) if (irandom(100) < _damageModule) { oPlayer.damagedModuleChance = irandom_range(1, 8); } global.playerTookDamage = true; if (_screenshake > 0) { global.screenshake = _screenshake; global.screenshake_time = _screenshakeTime; } // Hit effects var _effectX = x; var _effectY = y; if (instance_exists(_bulletInstance)) { _effectX = _bulletInstance.x; _effectY = _bulletInstance.y; } if (_hitParticle != noone) { part_particles_burst(global.partSystem, _effectX, _effectY, _hitParticle); } if (_hitSound != noone) { oSoundManager.playSoundAt(_hitSound, _effectX, _effectY, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 2, 1, random_range(0.8, 1.2)); } show_debug_message("Player Damage: Base=" + string(_damage) + ", Pen=" + string(_penetration) + ", Armor=" + string(_targetArmor) + ", NoDmg=" + string(_noDamage) + ", Ratio=" + string(_penetrationRatio) + ", Final=" + string(_effectiveDamage)); if (instance_exists(_bulletInstance) && _destroy) { instance_destroy(_bulletInstance); } return { damageDealt: _effectiveDamage, ricochet: false, effectiveDamage: _effectiveDamage, penetrationRatio: _penetrationRatio }; }
gmlfunction ApplyDamage(_target, _damage, _penetration, _damageModule = 10, _hitSound = noone, _hitParticle = noone, _ricochetParticle = noone, _ricochetSound = noone, _ricochetChance = 20, _bulletInstance = self, _destroy = true) { if (!instance_exists(_target)) { exit; } // Get target armor var _targetArmor = 1; if (variable_instance_exists(_target, "armor")) { _targetArmor = max(1, _target.armor); } // If penetration is 3 levels below armor -> no damage var _noDamage = (_penetration <= (_targetArmor - 3)); // Compute effective damage only if we can damage var _penetrationRatio = 0; var _effectiveDamage = 0; if (!_noDamage) { _penetrationRatio = _penetration / _targetArmor; _penetrationRatio = clamp(_penetrationRatio, 0.25, 1.75); _effectiveDamage = _damage * _penetrationRatio; _effectiveDamage = max(0.01, _effectiveDamage); } // ------------------------------------------------------------ // Ricochet // Simple: rear semicircle (towards where bullet came from) // Prevent re-collision with same target // ------------------------------------------------------------ if (irandom(100) < _ricochetChance) { if (instance_exists(_bulletInstance)) { var bulletAngle = _bulletInstance.direction; var ricochetAngle = bulletAngle + 180 + random_range(-90, 90); ricochetAngle = (ricochetAngle + 360) mod 360; _bulletInstance.direction = ricochetAngle; _bulletInstance.image_angle = _bulletInstance.direction; _bulletInstance.speed *= random_range(0.3, 0.8); _bulletInstance.flicker = true; // IGNORE the object we ricocheted off _bulletInstance.ricochetIgnoreId = _target.id; _bulletInstance.ricochetIgnoreTime = 12; // frames; tune as needed } if (_ricochetParticle != noone && instance_exists(_bulletInstance)) { part_particles_burst(global.partSystem, _bulletInstance.x, _bulletInstance.y, _ricochetParticle); } if (_ricochetSound != noone && instance_exists(_bulletInstance)) { oSoundManager.playSoundAt(_ricochetSound, _bulletInstance.x, _bulletInstance.y, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 1, 1, random_range(0.8, 1.2)); } // Ricochet chip damage only if not a total no-damage case if (!_noDamage && variable_instance_exists(_target, "hp")) { _target.hp -= max(0.01, (_effectiveDamage * 0.1)); } return { damageDealt: 0, ricochet: true, effectiveDamage: 0 }; } // ------------------------------------------------------------ // Normal hit (no ricochet) // ------------------------------------------------------------ if (!_noDamage && variable_instance_exists(_target, "hp")) { _target.hp -= _effectiveDamage; } // else: no damage at all if (variable_instance_exists(_target, "bulletFlinch")) { _target.image_angle += irandom_range(-_target.bulletFlinch, _target.bulletFlinch); } if (variable_instance_exists(_bulletInstance, "byPlayer") && _bulletInstance.byPlayer == true) { if (variable_instance_exists(_target, "damagedByPlayer")) { _target.damagedByPlayer = true; } } // Hit effects var _effectX = x; var _effectY = y; if (instance_exists(_bulletInstance)) { _effectX = _bulletInstance.x; _effectY = _bulletInstance.y; } if (_hitParticle != noone) { part_particles_burst(global.partSystem, _effectX, _effectY, _hitParticle); } if (_hitSound != noone) { oSoundManager.playSoundAt(_hitSound, _effectX, _effectY, global.sndBulletHitDist, global.sndBulletHitDist * 3, false, 2, 1, random_range(0.8, 1.2)); } show_debug_message("NPC Damage to " + (variable_instance_exists(_target, "infoName") ? _target.infoName : object_get_name(_target.object_index)) + ": Base=" + string(_damage) + ", Pen=" + string(_penetration) + ", Armor=" + string(_targetArmor) + ", NoDmg=" + string(_noDamage) + ", Ratio=" + string(_penetrationRatio) + ", Final=" + string(_effectiveDamage)); if (instance_exists(_bulletInstance) && _destroy) { instance_destroy(_bulletInstance); } return { damageDealt: _effectiveDamage, ricochet: false, effectiveDamage: _effectiveDamage, penetrationRatio: _penetrationRatio }; }
This keeps your “damage depends on armor”, but makes it consistent + safe for any object:
other.armor inside with that might not existgmlfunction Blastwave(blastwaveRadius, blastwaveStrength, damageNPC, damagePlayer, shakeTime, shakeStrenght, byPlayer, moduleDamage) { with (grpPhysics) { var dist = point_distance(other.x, other.y, x, y); if (dist < blastwaveRadius) { // Ensure armor exists var a = 1; if (variable_instance_exists(id, "armor")) { a = max(1, armor); } else { armor = 1; a = 1; } var angleToBlastwave = point_direction(other.x, other.y, x, y); // Push depends only on distance + mass (as you had) var pushStrength = ((blastwaveRadius - dist) / blastwaveRadius) * blastwaveStrength / max(1, mass); // Damage depends on distance and armor // (blastwaveStrength/100 is your scaling idea; keep it) var dmgFactor = ((blastwaveRadius - dist) / blastwaveRadius) * (blastwaveStrength / 100); dmgFactor /= a; // Apply push if (object_index != oM22C) { motion_add(angleToBlastwave, pushStrength); } // Player damage if (object_index == oPlayer) { var playerDamage = round(dmgFactor * damagePlayer); playerHealth -= playerDamage; show_debug_message("Blastdamage to Player: " + string(playerDamage)); show_debug_message("Player Health: " + string(playerHealth)); global.screenshake = pushStrength * shakeStrenght; global.screenshake_time = shakeTime; global.playerTookDamage = true; if (irandom(100) < moduleDamage) { damagedModuleChance = irandom_range(0, 8); } } // Mark hostiles damaged by player (unchanged intent) if (byPlayer) { with (grpHostiles) { damagedByPlayer = true; } } // NPC damage if (variable_instance_exists(id, "hp")) { var npcDamage = round(dmgFactor * damageNPC); hp -= npcDamage; show_debug_message((infoName != undefined ? infoName : object_get_name(object_index)) + " received blastdamage: " + string(npcDamage)); show_debug_message("NPC Health: " + string(hp)); } } } }
These scripts set:
bullet.ricochetIgnoreIdbullet.ricochetIgnoreTimeSo your bullet collision code needs to skip the collision when appropriate.
Add this near the top of the bullet collision handler (where the bullet hits a target):
gmlif (variable_instance_exists(id, "ricochetIgnoreTime") && ricochetIgnoreTime > 0) { if (other.id == ricochetIgnoreId) { exit; } }
And in the bullet Step event (or wherever):
gmlif (variable_instance_exists(id, "ricochetIgnoreTime") && ricochetIgnoreTime > 0) { ricochetIgnoreTime -= 1; }
That’s what actually enforces “can’t collide with the thing it ricocheted off”.
If you paste your bullet collision event (player + npc + armorplate), I can wire the ignore check in the exact right spot so ricochets behave perfectly across grpHostiles and oArmorPlate.
Please check this code, the textToDraw 1/2/3 are not drawn. Keep all the conditions (FCS)
oCamera Draw GUI:
image_speed = 0.1;
// Get screen dimensions
var camWidth = display_get_gui_width();
var camHeight = display_get_gui_height();
global.camWidth = camWidth;
global.camHeight = camHeight;
// Reset drawing settings
shader_reset();
draw_set_font(fontOrbS);
draw_set_alpha(1);
draw_set_halign(fa_center);
draw_set_valign(fa_bottom);
// Navigation constants
var enemyIndicatorDist = 3500;
var maxSpriteSize = 80;
var borderOffset = 150;
// Target groups for navigation indicators
var groups = [
{ group: grpHostileShips, color: c_red, label: "Hostile" },
{ group: grpAlliedShips, color: c_lime, label: "Ally" },
{ group: grpPickups, color: c_orange, label: "Pickup" }
];
// Refuel Pad Safe Zone
if (room == rmSpace1 && instance_exists(oRefuelPad))
{
var baseScreenX = oRefuelPad.x - (oCamera.x - camWidth / 2);
var baseScreenY = oRefuelPad.y - (oCamera.y - camHeight / 2);
textdraw_set_alpha(0.8); draw_ellipse_color( baseScreenX - 2000, baseScreenY - 2000, baseScreenX + 2000, baseScreenY + 2000, c_lime, c_lime, true ); draw_set_alpha(1);
}
// Draw HUD if player exists
if (instance_exists(oPlayer))
{
var mouseX = device_mouse_x_to_gui(0);
var mouseY = device_mouse_y_to_gui(0);
text// Fire Control System indicators if (!oPlayer.damagedFCS) { // Autocannon Proxy indicator if (oPlayer.proxy && cursorTextTime1 > 0) { cursorTextTime1--; draw_set_valign(fa_middle); draw_text_colour(mouseX, mouseY + 2, "PROXY", c_aqua, c_aqua, c_aqua, c_aqua, 0.6); draw_set_valign(fa_bottom); draw_set_alpha(0.2); draw_circle_colour(mouseX, mouseY, 80, c_aqua, c_aqua, true); draw_set_alpha(1); } else { cursorTextTime1 = 180; } // Autocannon APHE indicator if (oPlayer.mainWeapon == 2 && oPlayer.mainWeaponMode == 2 && cursorTextTime2 > 0) { cursorTextTime2--; draw_set_valign(fa_middle); draw_text_colour(mouseX, mouseY + 2, "APHE", c_aqua, c_aqua, c_aqua, c_aqua, 0.6); draw_set_valign(fa_bottom); } else { cursorTextTime2 = 180; } // Draw text tooltip if active if (drawText) { var textY = camHeight - 250; draw_text_color(camWidth / 2, textY, textToDraw1, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 15, textToDraw2, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 30, textToDraw3, textColor, textColor, textColor, textColor, 1); } } // Navigation indicators (if navigation not damaged) if (!oPlayer.damagedNavigation) { // Player position relative to camera for GUI calculations var playerScreenX = camWidth / 2; var playerScreenY = camHeight / 2; // Process each group for navigation indicators for (var i = 0; i < array_length(groups); i++) { var groupData = groups[i]; var group = groupData.group; var color = groupData.color; var label = groupData.label; if (instance_exists(group)) { var tgt = instance_nearest(oPlayer.x, oPlayer.y, group); if (tgt != noone) { var tgtDist = point_distance(oPlayer.x, oPlayer.y, tgt.x, tgt.y); // Convert target to GUI coordinates var tgtScreenX = tgt.x - (oCamera.x - playerScreenX); var tgtScreenY = tgt.y - (oCamera.y - playerScreenY); // Check if target is outside GUI viewport var inGUIView = (tgtScreenX >= 0 && tgtScreenX <= camWidth && tgtScreenY >= 0 && tgtScreenY <= camHeight); if (!inGUIView && tgtDist < enemyIndicatorDist) { // Calculate direction from player to target in GUI space var pDir = point_direction(playerScreenX, playerScreenY, tgtScreenX, tgtScreenY); var dirX = lengthdir_x(1, pDir); var dirY = lengthdir_y(1, pDir); // GUI viewport rectangle for clamping var rectLeft = borderOffset; var rectRight = camWidth - borderOffset; var rectTop = borderOffset; var rectBottom = camHeight - borderOffset; // Initialize indicator position in GUI space var edgeX = playerScreenX + dirX * 10000; var edgeY = playerScreenY + dirY * 10000; // Clamp to GUI edges edgeX = clamp(edgeX, rectLeft, rectRight); edgeY = clamp(edgeY, rectTop, rectBottom); // Refine position to lie exactly on the border var t = infinity; var finalX = edgeX; var finalY = edgeY; if (dirX != 0) { var tLeft = (rectLeft - playerScreenX) / dirX; var tRight = (rectRight - playerScreenX) / dirX; if (tLeft > 0) { var yLeft = playerScreenY + tLeft * dirY; if (yLeft >= rectTop && yLeft <= rectBottom && tLeft < t) { t = tLeft; finalX = rectLeft; finalY = yLeft; } } if (tRight > 0) { var yRight = playerScreenY + tRight * dirY; if (yRight >= rectTop && yRight <= rectBottom && tRight < t) { t = tRight; finalX = rectRight; finalY = yRight; } } } if (dirY != 0) { var tTop = (rectTop - playerScreenY) / dirY; var tBottom = (rectBottom - playerScreenY) / dirY; if (tTop > 0) { var xTop = playerScreenX + tTop * dirX; if (xTop >= rectLeft && xTop <= rectRight && tTop < t) { t = tTop; finalX = xTop; finalY = rectTop; } } if (tBottom > 0) { var xBottom = playerScreenX + tBottom * dirX; if (xBottom >= rectLeft && xBottom <= rectRight && tBottom < t) { t = tBottom; finalX = xBottom; finalY = rectBottom; } } } // Draw Line and Circle in GUI space draw_line_color(finalX, finalY, tgtScreenX, tgtScreenY, color, color); draw_circle_color(finalX, finalY, 48, color, color, true); // Draw Text draw_set_halign(fa_center); draw_set_valign(fa_middle); draw_set_color(color); draw_text(finalX, finalY - 60, tgt.infoName != undefined ? tgt.infoName : label); draw_text(finalX, finalY + 65, string(floor(tgtDist))); // Scale and Draw Sprite var scaleX = maxSpriteSize / tgt.sprite_width; var scaleY = maxSpriteSize / tgt.sprite_height; var scale = min(scaleX, scaleY); draw_sprite_ext(tgt.sprite_index, tgt.image_index, finalX, finalY, scale, scale, tgt.image_angle, color, 1); } } } } } // Hostile Ship UI Overlay (enemy health bars/circles) if (instance_exists(grpHostileShips) && instance_number(grpHostileShips) > 0) { with (grpHostileShips) { if (!instance_exists(id)) continue; // Convert enemy position to GUI coordinates var enemyScreenX = x - oCamera.x + camWidth / 2; var enemyScreenY = y - oCamera.y + camHeight / 2; // Check if enemy is within firing distance of player if (distance_to_object(oPlayer) < fireDist) { // Set drawing settings draw_set_font(fontOrbS); draw_set_halign(fa_center); draw_set_valign(fa_middle); draw_set_color(c_red); draw_set_alpha(0.6); // Draw health text var healthText = "HP: " + string(hp); draw_text(enemyScreenX, enemyScreenY - circleRadius - 15, healthText); // Draw targeting circle draw_circle(enemyScreenX, enemyScreenY, circleRadius, true); // Reset alpha for next drawing draw_set_alpha(1); } } } // Visual effects for damage if (global.playerTookDamage) { draw_sprite_ext(texRedScreen, 0, camWidth / 2, camHeight / 2, 1, 2, 0, -1, 1); draw_set_color(c_red); } if (global.playerHitMissile) { draw_sprite_ext(texGlitch1, random(9), camWidth / 2, camHeight / 2, 2, 2, 0, -1, 0.2); if (alarm[0] <= 0) alarm_set(0, 20); } // Dialogue text if (drawTextDialogue) { draw_text_color(camWidth / 2, camHeight - 325, textDialogue, textDialogueColor, textDialogueColor, textDialogueColor, textDialogueColor, 1); } // Object tooltips on mouse hover infoDrawn = false; // Check collisions with various object types if (collision_circle(mouseX, mouseY, 28, grpPlanets, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, grpPlanets); if (instance_exists(mouseOver)) { textColor = c_aqua; drawText = true; alarm_set(11, 100); textToDraw3 = mouseOver.infoName; textToDraw2 = $"| Fuel: {mouseOver.fuel} | Ammo: {mouseOver.ammo} |"; textToDraw1 = $"| Mines: {mouseOver.mines} | Factories: {mouseOver.factories} |"; } } if (collision_circle(mouseX, mouseY, 28, grpMeteors, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, grpMeteors); if (instance_exists(mouseOver)) { textColor = c_white; drawText = true; alarm_set(11, 100); textToDraw3 = " "; textToDraw2 = mouseOver.infoName; textToDraw1 = $"| Condition: {mouseOver.hp} |"; } } if (collision_circle(mouseX, mouseY, 18, oPlayer, true, true)) { textColor = c_lime; drawText = true; alarm_set(11, 100); textToDraw1 = oPlayer.infoName; textToDraw2 = " "; textToDraw3 = " "; } if (collision_circle(mouseX, mouseY, 64, oM21Bastion, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oM21Bastion); if (instance_exists(mouseOver)) { textColor = c_lime; drawText = true; alarm_set(11, 100); textToDraw1 = mouseOver.infoName; textToDraw2 = " "; textToDraw3 = " "; } } if (collision_circle(mouseX, mouseY, 18, oM22C, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oM22C); if (instance_exists(mouseOver)) { textColor = c_aqua; drawText = true; alarm_set(11, 100); textToDraw3 = mouseOver.infoName; if (mouseOver.destination != noone) { textToDraw2 = $"| Destination: {mouseOver.destination} | Speed: {round(mouseOver.speed)} |"; } else { textToDraw2 = $"| Destination: NONE | Speed: {round(mouseOver.speed)} |"; } textToDraw1 = $"| Condition: {mouseOver.hp} | Fuel: {mouseOver.fuel} | Ammo: {mouseOver.ammo} |"; } } if (collision_circle(mouseX, mouseY, 18, oM22CDestroyed, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oM22CDestroyed); if (instance_exists(mouseOver)) { textColor = c_white; drawText = true; alarm_set(11, 100); textToDraw2 = mouseOver.infoName; textToDraw1 = $"| Condition: {mouseOver.hp} |"; } } if (collision_circle(mouseX, mouseY, 18, oCargo, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oCargo); if (instance_exists(mouseOver)) { textColor = c_orange; drawText = true; alarm_set(11, 100); textToDraw3 = " "; textToDraw2 = mouseOver.infoName; textToDraw1 = $"| Condition: {mouseOver.hp} |"; } } if (collision_circle(mouseX, mouseY, 32, oRefuelPad, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oRefuelPad); if (instance_exists(mouseOver)) { textColor = c_lime; drawText = true; alarm_set(11, 100); textToDraw3 = mouseOver.infoName; textToDraw2 = $"| Fuel: {mouseOver.fuel} | Ammo: {mouseOver.ammo} |"; textToDraw1 = mouseOver.info; } } if (collision_circle(mouseX, mouseY, 24, grpHostiles, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, grpHostiles); if (instance_exists(mouseOver)) { textColor = c_red; drawText = true; alarm_set(11, 100); textToDraw3 = " "; textToDraw2 = mouseOver.infoName; textToDraw1 = $"| Condition: {mouseOver.hp} | HOSTILE! |"; } } if (collision_circle(mouseX, mouseY, 24, oEnemyBase1, true, true)) { var mouseOver = instance_nearest(mouseX, mouseY, oEnemyBase1); if (instance_exists(mouseOver)) { textColor = c_red; drawText = true; alarm_set(11, 100); textToDraw3 = " "; textToDraw2 = mouseOver.infoName; textToDraw1 = $"| Condition: {mouseOver.hp} | HOSTILE! |"; } } // Health-based visual effects if (round(global.playerHealth) < 40) { var ph = global.playerHealth / 40; draw_sprite_ext(texRedScreen, 0, camWidth / 2, camHeight / 2, 1 + ph / 16, 1 + ph / 9, 0, -1, 1); } // Main HUD display if (round(global.playerHealth) < 30) { draw_sprite_ext(texHUD, 1, camWidth / 2, camHeight, 1, 1, 0, -1, 1); draw_sprite(texStatusCritical, -1, camWidth / 2, camHeight - 1000); hudColor = c_red; hudBGColor = c_red; } else { draw_sprite_ext(texHUD, 0, camWidth / 2, camHeight, 1, 1, 0, hudBGColor, 1); hudBGColor = c_white; hudColor = c_aqua; } // Draw "HEALTH" text above health bar draw_set_font(fontOrbS1); draw_set_halign(fa_center); draw_set_valign(fa_middle); // Fuel indicators var fuelY = camHeight - 950; if (global.playerFuel > 25) { // Fuel is good - don't show warning // draw_sprite(texFullFuel, 0, camWidth / 2, fuelY); } else if (global.playerFuel <= 25 && global.playerFuel > 10) { draw_sprite(texLowFuel1, image_index, camWidth / 2, fuelY); } else if (global.playerFuel <= 10 && global.playerFuel > 0) { draw_sprite(texLowFuel2, image_index, camWidth / 2, fuelY); } else if (global.playerFuel <= 0) { draw_sprite(texNoFuel, image_index, camWidth / 2, fuelY); } // Health display draw_set_font(fontOrbL); draw_set_halign(fa_center); draw_set_valign(fa_bottom); // Bottom alignment for health number if (round(global.playerHealth) < 30 || global.playerTookDamage) { draw_set_color(c_red); } else { draw_set_color(c_aqua); } draw_text(camWidth / 2, camHeight - 24, string(round(global.playerHealth))); // Coordinates draw_set_font(fontOrbS); draw_set_halign(fa_center); draw_set_valign(fa_bottom); // Bottom alignment for coordinates draw_text(camWidth / 2 - 105, camHeight - 6, $"X {round(oPlayer.x / 1000)}"); draw_text(camWidth / 2 + 115, camHeight - 6, $"Y {round(oPlayer.y / 1000)}"); // Ammo counters var ammoY = camHeight - 28; draw_set_halign(fa_center); draw_set_valign(fa_bottom); // Bottom alignment for ammo counters draw_text(camWidth / 2 - 166, ammoY, $"RKT\n{global.playerAmmo3}"); draw_text(camWidth / 2 - 126, ammoY, $"MSL\n{global.playerAmmo2}"); draw_text(camWidth / 2 - 86, ammoY, $"FLR\n{global.playerFlares}"); // Resources draw_set_halign(fa_left); draw_set_valign(fa_bottom); draw_text(camWidth / 2 + 71, camHeight - 42, $"DETAILS {global.playerScrap}"); draw_text(camWidth / 2 + 71, camHeight - 28, $"CREDITS {round(global.playerCredits)}"); draw_set_color(hudBGColor); draw_text(camWidth / 2 + 74, camHeight - 72, $"FUEL {global.playerFuel}"); draw_text(camWidth / 2 - 156, camHeight - 72, $"ENERGY {round(global.playerEnergy / 10)}"); // Message System draw_set_font(fontOrbS1); var yOffset = 870; for (var i = ds_list_size(messages) - 1; i >= 0; i--) { draw_set_halign(fa_center); draw_set_valign(fa_middle); var msg = messages[| i]; msg[1] -= 1; var fadeAlpha = 1.0; if (msg[1] <= messagesFadeTime) { fadeAlpha = msg[1] / messagesFadeTime; } if (msg[1] <= 0) { ds_list_delete(messages, i); continue; } draw_text_color(camWidth / 2, yOffset, msg[0], msg[2], msg[2], msg[2], msg[2], fadeAlpha); yOffset += 25; } // XP Progress bar draw_set_halign(fa_middle); draw_set_valign(fa_bottom); draw_set_font(fontOrbS); var xpRatio = oPlayer.xp / oPlayer.xpNextLevel; var barCenterX = camWidth / 2; var barY = camHeight - 10; var barWidth = 154; draw_rectangle_colour( barCenterX - 80, barY, barCenterX + 79, barY - 10, c_teal, c_teal, c_teal, c_teal, false ); draw_rectangle_colour( barCenterX - 78, barY - 2, barCenterX - 77 + (barWidth * xpRatio), barY - 8, c_aqua, c_aqua, c_aqua, c_aqua, false ); //draw_text(barCenterX, barY - 3, $"Level: {oPlayer.level}");
}
// Store prompt
if (showStorePrompt)
{
var screenWidth = display_get_gui_width();
var screenHeight = display_get_gui_height();
textvar promptX = screenWidth / 2; var promptY = screenHeight - 120; draw_set_font(fontOrbM); draw_set_halign(fa_center); draw_set_valign(fa_bottom); var pulse = abs(sin(current_time * 0.01)) * 0.5 + 0.5; var glowColor = make_color_rgb(0, 255 * pulse, 0); // Background draw_set_color(c_black); draw_set_alpha(0.8); draw_roundrect(promptX - 150, promptY - 28, promptX + 153, promptY + 25, false); draw_set_alpha(1); // Glow border draw_set_color(glowColor); draw_set_alpha(0.3); draw_roundrect(promptX - 152, promptY - 30, promptX + 155, promptY + 27, false); draw_set_alpha(1); // Text draw_set_color(c_lime); draw_text(promptX, promptY, storePromptText); draw_set_font(fontOrbS); draw_set_color(c_yellow); draw_text(promptX, promptY + 20, "Press TAB to Enter");
}
// Reset drawing settings
draw_set_alpha(1);
draw_set_halign(fa_center);
draw_set_valign(fa_bottom);
oCamera Alarm 0:
global.playerHitMissile = false
oCamera Alarm 10:
drawTextDialogue = false;
textDialogue = " ";
oCamera Alarm 11:
drawText = false;
textToDraw1 = " ";
textToDraw2 = " ";
textToDraw3 = " ";
And also add armor display for oArmorPlate object on mouse collision (after mouse collision the armor icon and text are displayed at mouse for X amount of time (alarm)
//ARMOR PLATE INFO (only on mouse collision)
var mouseOverArmorPlate = instance_position(mouse_x, mouse_y, oArmorPlate);
if (instance_exists(mouseOverArmorPlate))
{
var armorPlate = mouseOverArmorPlate;
text// Get armor and hp values var armorValue = 1; var hpValue = 100; if (variable_instance_exists(armorPlate, "armor")) { armorValue = armorPlate.armor; } if (variable_instance_exists(armorPlate, "hp")) { hpValue = armorPlate.hp; } // Calculate armor color based on penetration (if projectile exists) var armorColor = c_white; if (projectile != noone) { var _penetrationRatio = projectilePen / armorValue; _penetrationRatio = clamp(_penetrationRatio, 0, 2); if (_penetrationRatio >= 1.5) { armorColor = c_aqua; } else if (_penetrationRatio >= 1.0) { armorColor = c_lime; } else if (_penetrationRatio >= 0.75) { armorColor = c_yellow; } else if (_penetrationRatio >= 0.5) { armorColor = c_orange; } else { armorColor = c_red; } } // Draw armor plate icon at mouse position with vertical offset var drawX = mouse_x; var drawY = mouse_y + 55; // Vertical offset below mouse // Draw armor plate sprite with armorColor draw_sprite_ext(texIconArmorPlate, 0, drawX, drawY, 1, 1, 0, armorColor, 1); // Draw HP text above sprite draw_set_font(fontOrbS); draw_set_halign(fa_center); draw_set_colour(c_red); draw_text(drawX, drawY - 20, "HP: " + string(hpValue)); // Draw armor text at center of sprite draw_set_colour(c_black); draw_text(drawX, drawY, string(armorValue)); // Draw line from mouse to armor plate draw_set_colour(armorColor); draw_set_alpha(0.3); draw_line(mouse_x, mouse_y, armorPlate.x, armorPlate.y); draw_set_alpha(1);
}
Your textToDraw1/2/3 aren’t drawn because you draw them BEFORE you ever set them.
Right now the order is:
if (!oPlayer.damagedFCS) { ... if (drawText) draw_text(textToDraw...) } ✅ runs earlydrawText = true; textToDraw... = ...; alarm_set(11,100); ✅ runs afterdrawText is still false / old strings.Keep all your tooltip-setting logic exactly as-is, but move the tooltip draw block to after the mouse collision checks, still inside if (!oPlayer.damagedFCS).
gml// Draw text tooltip if active if (drawText) { var textY = camHeight - 250; draw_text_color(camWidth / 2, textY, textToDraw1, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 15, textToDraw2, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 30, textToDraw3, textColor, textColor, textColor, textColor, 1); }
if (instance_exists(oPlayer)) { ... } block,after all the mouse collision tooltip setters, but still gated by FCS:
Put this right after your last mouse collision block (after oEnemyBase1), and before the “Health-based visual effects” section:
gml// ------------------------------------------------------------ // Draw tooltip text LAST (so textToDraw1/2/3 is already assigned) // Keep FCS condition // ------------------------------------------------------------ if (!oPlayer.damagedFCS) { if (drawText) { var textY = camHeight - 250; draw_set_font(fontOrbS); draw_set_halign(fa_center); draw_set_valign(fa_bottom); draw_text_color(camWidth / 2, textY, textToDraw1, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 15, textToDraw2, textColor, textColor, textColor, textColor, 1); draw_text_color(camWidth / 2, textY + 30, textToDraw3, textColor, textColor, textColor, textColor, 1); } }
That alone will make textToDraw1/2/3 render correctly.
You asked: only on mouse collision show armor plate icon + armor + hp near the mouse for a while.
gmlshowArmorPlateHud = false; armorPlateHudArmor = 1; armorPlateHudHp = 0; armorPlateHudColor = c_white;
Put it near your other hover checks (same area), and keep it under the same FCS behavior by placing it inside the if (instance_exists(oPlayer)) { ... } block (it does NOT need to be inside the tooltip text system):
gml// ------------------------------------------------------------ // ARMOR PLATE INFO (only on mouse collision) // Shows icon+armor+hp at mouse for X time using an alarm // ------------------------------------------------------------ var mouseX = device_mouse_x_to_gui(0); var mouseY = device_mouse_y_to_gui(0); // Convert GUI mouse -> world position (so instance_position works correctly) var mouseWorldX = (oCamera.x - camWidth / 2) + mouseX; var mouseWorldY = (oCamera.y - camHeight / 2) + mouseY; var mouseOverArmorPlate = instance_position(mouseWorldX, mouseWorldY, oArmorPlate); if (instance_exists(mouseOverArmorPlate)) { var armorPlate = mouseOverArmorPlate; // read values armorPlateHudArmor = 1; armorPlateHudHp = 0; if (variable_instance_exists(armorPlate, "armor")) { armorPlateHudArmor = max(1, armorPlate.armor); } if (variable_instance_exists(armorPlate, "hp")) { armorPlateHudHp = armorPlate.hp; } // penetration color (if projectile exists) armorPlateHudColor = c_white; if (projectile != noone) { var r = projectilePen / armorPlateHudArmor; r = clamp(r, 0, 2); if (r >= 1.5) armorPlateHudColor = c_aqua; else if (r >= 1.0) armorPlateHudColor = c_lime; else if (r >= 0.75) armorPlateHudColor = c_yellow; else if (r >= 0.5) armorPlateHudColor = c_orange; else armorPlateHudColor = c_red; } showArmorPlateHud = true; alarm_set(12, 90); // X amount of time (frames). change 90 as desired. }
Place this somewhere after you set/reset draw settings (any time after you compute mouseX/mouseY is fine):
gmlif (showArmorPlateHud) { var mouseX = device_mouse_x_to_gui(0); var mouseY = device_mouse_y_to_gui(0); var drawX = mouseX; var drawY = mouseY + 55; draw_set_font(fontOrbS); draw_set_halign(fa_center); draw_set_valign(fa_middle); // icon draw_sprite_ext(texIconArmorPlate, 0, drawX, drawY, 1, 1, 0, armorPlateHudColor, 1); // HP above draw_set_colour(c_red); draw_text(drawX, drawY - 20, "HP: " + string(ceil(armorPlateHudHp))); // armor on icon draw_set_colour(c_black); draw_text(drawX, drawY, string(armorPlateHudArmor)); // reset draw_set_colour(c_white); }
gmlshowArmorPlateHud = false;
Your snippet used mouse_x/mouse_y (room coords) in a Draw GUI context. In GUI you should use device_mouse_x_to_gui() and convert to world if you want collision checks. The code above fixes that.
If you want, I can also make the plate HUD show armor+hp of the specific plate and draw a line to the plate’s on-screen position (GUI-space) without mixing coordinate spaces.