Hello, I'm making a Top-Down Space Shooter game in...

作成日: 2026年1月9日

使用モデル GPT-5.2 by Chat01

質問

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:

  • Sounds: sfxSound, sfxSound2
  • Sprites: texSprite
  • Music: musMusic
  • Groups (parent objects): grpGroup
  • Rooms: rmRoom1
  • Scripts: scScript
    write each event seperately

What I already have:

  • a menu room
  • a 100k x 100k open-world room
  • Save/Load System
  • Player object that can move, shoot guns, rockets, guided missles, laser, flamethrower, use energy shield, dash, pop flares
  • Player XP and Level-Up system
  • Player Modules System: Engine, Thrusters, Main Weapon, Weapons Bay, Defenses, Navigation
  • Store where the player can buy new weapons, ammo, fuel and repair, and target practice
  • Threat Level System and Music Manager that chooses playlist depending on Threat Level
  • 3 enemy types: oEnemyS1 (small agile with slow firing gun), oEnemyM1 (medium, slow, fires guided missles at long range and rapid laser at close range), oEnemyM2 (medium, fast, agile, rapid-fire 3 round burst gun)
  • Environmental hazard: Gas Cloud, damages everything inside it and disables players modules
  • Wormhole: sucks everything and teleports to random location

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;

思考

Providing GML code and structuring enemy behavior

I need to write GML code for oEnemyL1 events, following Allman indentation and lowerCamelCase. I’ll define behavior like patrol, chase, rocketArming, rocketFiring/reloading, and cannon actions. I'll keep flares code mostly as-is and assume oEnemyL1Danger is an object for the rocket alarm. For the rocket barrage, I’ll spawn multiple rockets along a line following the rotation and position, and I’ll avoid particles, sounds, and lock-on elements.

Implementing rocket barrage parameters

I need to add an outward shift for the rocket barrage, with a default value of 8. This means the shift is perpendicular to the facing direction. For a left barrage, the shift is upward, and for a right barrage, it's downward. I'll use oRocket1 for the rockets, setting their direction slightly varied, and adjust the speed accordingly. The reload system will limit it to 3 barrages before a cooldown. After each shot, I'll track the remaining shots and handle the cooldown appropriately.

Thought for 19s

回答

Below is a clean oEnemyL1 implementation that replaces the old M1 missile/lock-on/laser logic with:

  • Patrol (player out of range)
  • Chase / Close in (player in range, not yet in firing band)
  • Rocket Barrage (arm delay + danger indicator + fire + reload, 3 barrages max before longer reload)
  • Close-range Cannons (2 muzzles, alternating L>R x3, fast shot spacing, then short burst reload)
  • Keeps your flares behavior (same structure, simplified a bit)

No particles, no draw calls, no sounds.

All code uses lowerCamelCase and Allman indentation.
Each event is separate.


oEnemyL1 — Create Event

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

oEnemyL1 — Step Event

gml
event_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.

oEnemyL1 — Alarm 0 Event (Rocket fire gate)

gml
// Simple gate: re-enable rocket firing next step (prevents double-trigger same frame) rocketCanFire = true;

oEnemyL1 — Alarm 2 Event (Optional: if you want to re-use old pattern)

gml
// Not used by this L1 setup, but kept to avoid breaking inherited logic if something calls it. cannonCanFire = true;

oEnemyL1 — Alarm 11 Event (Flare Cooldown)

gml
canDeployFlares = true;

Notes you can tweak quickly

  • rocketArmTime: how long the “danger” telegraph lasts before firing.
  • rocketsPerSide: rockets along each diagonal.
  • rocketOutShift: increase if rockets still clip the ship.
  • rocketReloadTime / rocketMagazineReloadTime: pacing.
  • cannonShotDelay / cannonBurstReload: cannon rhythm.

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

思考

Modifying rocket firing sequence

I need to modify the code to make the rockets fire sequentially from inner to outer points, alternating from left to right. This means adjusting timers and precomputing the spawn points for both sides. The inner points are closer to the ship's center, and the firing sequence must account for the correct order based on proximity to the midline. I'll define the order carefully, considering left and right side distinctions.

Firing rockets in alternating sequence

I need to implement the firing sequence for rockets, alternating between left and right sides while maintaining symmetry. Instead of firing pairs simultaneously, I'll fire one rocket at a time, first left, then right, keeping the same ring index for each step. I'll use timers and adjust the firing direction based on the computed lead, not just the ship's angle. I'll allow independent firing direction control while using the image angle to spawn the rockets.

Adjusting enemy targeting and AI behavior

I need to modify the targeting logic since the player only shoots when directly facing the enemy. I’ll remove the line-of-sight requirement for rockets and cannons, using a lead direction for both. The AI needs strafing and avoidance behavior using collision checks. I'll implement patrol points when no player is nearby, and refine behavior like steering and random movement to avoid obstacles using collision detection.

Thought for 20s

回答

Got it — two fixes + a smarter movement brain:

  1. Leading aim: rockets/cannons will aim at a lead point, not only when the ship is perfectly facing the player (no more “only shoots when directly facing”).
  2. Rocket barrage order: rockets fire in turn, inner → outer, alternating Left then Right so the wings are symmetric over the sequence.
  3. Advanced AI: patrol with wander points, strafe in combat, and avoid collisions vs 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.


oEnemyL1 — Create Event

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

oEnemyL1 — Step Event

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

oEnemyL1 — Alarm 11 Event (Flare Cooldown)

gml
canDeployFlares = true;

What changed (directly addressing your issues)

  • 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

    • Patrol uses a wander point inside guardRadius around homeX/homeY
    • Combat uses distance banding + strafe in the firing zone
    • Avoidance checks ahead vs grpCollide and adds lateral steering
    • Light separation keeps enemies from stacking

If 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, E5FAFF,E5FAFF, 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, FFFFFF,FFFFFF, 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,4C4C4C, 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,4C4C4C, 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,4C4C4C, 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, 0055FF,0055FF, 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);

思考

Refining cannon setup to prevent leaks

I need to adjust the cannon's shooting mechanics, using rare bursts with cooldown and ensuring it's close enough for cannons to work effectively. The provided particle code is heavy, though, so I'll want to create the particle system in the Create event instead of on each shot. I'll set up separate smoke types for left and right cannons and make sure to destroy the particle system properly to avoid leaks later.

Thought for 7s

回答

Here are the changes you asked for, implemented cleanly:

  • Strafing is rare: it now happens in short bursts with a cooldown (no constant zig-zag).
  • It will close in to cannon range: when cannons are relevant, it pushes into closeDist instead of hovering at fireDist.
  • Danger indicator is commented out (kept in place so you can re-enable later).
  • Every cannon shot spawns your particle effect — but optimized: the particle system/types are created once in Create, then burst per shot.
  • SmokeSide is mirrored on the right cannon (left uses 90°, right uses 270°).

Below are updated events for oEnemyL1.


oEnemyL1 — Create Event

gml
event_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); };

oEnemyL1 — Step Event

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

oEnemyL1 — Alarm 11 Event (Flare Cooldown)

gml
canDeployFlares = true;

oEnemyL1 — Destroy Event (important to prevent particle leaks)

gml
if (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); }

Quick tuning knobs (if you want it even rarer)

  • 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.


1) Best performance particle setup (GLOBAL, shared)

✅ Script: scL1CannonParticlesInit

Call 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);

✅ Script: scL1CannonMuzzleBurst

This 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:

  • no per-enemy part systems
  • only one system, reused
  • per shot: just part_system_position + bursts.

2) oEnemyL1 changes: cannon distance logic + particle call

oEnemyL1 — Create Event (only the cannon range section changed)

Add these:

gml
closeDist = 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).


oEnemyL1 — Step Event (movement logic for cannon closing)

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:

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

oEnemyL1 — Step Event (cannon firing distance check + particle burst)

Replace your cannon logic with this:

gml
var 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:

  • At the muzzle world position
  • Facing image_angle
  • With mirrored SmokeSide automatically

3) What to do with the particle init script (important!)

Call scL1CannonParticlesInit() once in one of these spots:

Best option:

oGameManager Create Event

gml
scL1CannonParticlesInit();

Or:

Room Start event of your world room.

That ensures the global particle resources exist before any L1 shoots.


4) New gameplay ideas for this ship (L1 “Hurricane”)

Here are mechanics that match your current systems (threat level, modules, gas cloud, wormhole, open world):


A) “Barrage Lock” (pressure mechanic)

When L1 arms rockets, it creates a temporary targeting field:

  • Rockets become slightly faster
  • Player dash cooldown increases for 1 second (fits your module system without hard disabling)
    Gives the “Hurricane” identity as a suppression ship.

Counterplay: Shield blocks / flare / break line-of-sight.


B) “Drive-by” cannon behavior (feels like a heavy hunter)

Instead of stopping close:

  • It tries to pass by the player at closeDist
  • Fires cannons during the pass (L>R x3)
  • Then “loops” and re-engages

This 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.)


C) “Armor Plate” weakspot mechanic (visual reward)

Because it’s heavy:

  • It has 2 side armor plates
  • When HP < 50%, plates “break”
  • After that, cannons fire faster OR rockets reload faster

This makes it feel like you’re peeling it apart.


D) “Rocket Curtain”

Instead of rockets aimed at player, rockets fire in a slightly arcing curtain:

  • half aim at lead
  • half aim slightly offset left/right This forces movement and makes the ship feel like a zoning boss without being unfair.

E) Interaction with Gas Cloud (cool emergent behavior)

If the L1 enters gas cloud:

  • Modules disabled like player
  • It becomes more aggressive with cannons only
  • Rockets disabled (nice lore: rockets won’t ignite)

This makes hazards feel alive and improves sandbox combat.


F) “Escort Leader”

When L1 exists, smaller enemies behave differently:

  • S1 ships protect it, staying near its sides
  • M2 ships do flanking runs This creates fleet “formations” and makes your threat level system feel richer.

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

思考

Planning armor logic and damage handling

I need to create GML code for the armor system. The oArmorPlate object will interact with the FOV logic and the End Step armor handling—applying damage reduction. I’ll have oEnemyL1 create armor plates, tracking their status, lastKnown, and FOV. Plates will reduce damage when hit, and damage gets routed through them. I'll compute effective armor by summing base armor and plate contributions.

Implementing armor plates and FOV logic

I’ll create plates using offset anchors, matching the ship’s x, y, and rotation with its image_angle. Plates will have adjustable properties like sprite_index, maxHp, armor, and ricochetMult. For FOV, I’ll check if the player is within distance and angle using angle_difference, allowing for patrol while accounting for ship rotation. When the player is detected, the ship's engagement state changes and lastKnown is updated.

Thought for 13s

回答

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).
  • Plates sit at the ship’s x/y and rotate with image_angle (no offsets required).
  • Plates die with your particle burst.
  • oEnemyL1 gets FOV + last known position + player-lost countdown.
  • Armor is applied in the ship’s End Step (aggregates plate stats into ship stats).

Below is the code.


Object: oArmorPlate

Create Event

gml
event_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;

Step Event

gml
event_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;

Destroy Event

gml
event_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 oArmorPlate when they collide with it (since it has its own collision mask). Keep damage logic where it already exists (bullet collision event, etc.).


Object: oEnemyL1

Create Event (additions + armor plate spawn + FOV state)

gml
event_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/B with whatever sprite names you choose.


Step Event (FOV engage + last known + lost countdown + patrol reset)

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:

gml
event_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

Cannon distance gating (simple rule)

Use these bands:

  • Cannons: dist <= shootDist
  • Rockets: shootDist < dist <= fireDist

That produces “close to cannon, mid to rockets”.


End Step Event (Armor plate aggregation happens here)

This is what you asked: the ship’s armor/ricochetMult is computed in End Step, based on live plates.

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

5) New ideas for L1 that fit your armor plate + FOV mechanics

1) “Blind Spot” weak side

Give each plate a different collision zone:

  • Left plate blocks bullets better from the front-left arc
  • Right plate blocks from front-right arc
    Players learn to orbit to expose the uncovered angle.

2) “Search pattern” when losing FOV

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.

3) “Plate break triggers behavior”

When a plate breaks:

  • brief “panic thrust” (short speed burst)
  • then it becomes more cannon aggressive (pushes to closeDist harder) This gives a clear phase change that players can read.

4) “Sensor cone” counterplay with flares / gas cloud

If it enters Gas Cloud (modules disabled):

  • FOV halves (fovHalfAngle *= 0.6)
  • cannons only (rockets disabled) So hazards become tactical tools.

5) “Escort beacon” (threat synergy)

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

思考

Considering event triggers and object interactions

I need to implement the events for oArmorPlate and oEnemyL1, including creating, stepping, ending steps, and destruction. Also, I'll handle alarms for flares and rockets. I’ll factor in collision detection, including with the gas cloud object (oGasCloud). I'll set up conditions for breaking plates, ram behavior, and speed adjustments based on gas cloud interactions.

Defining ram behavior and searching mechanics

For the ram, I’ll set the direction toward the player (or last known position) and apply high acceleration with a speed cap around 12. The ram will stop if it collides with the target. For the search pattern, I’ll use a circular search around the last known position, gradually increasing the radius and angle. I'll also ensure shooting distances and particle effects are properly defined.

Thought for 11s

回答

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:

  • lowerCamelCase
  • Allman indentation
  • no danger indicator (commented out)
  • cannons: closeDist = 800, shootDist = 1200
  • rockets: inner→outer, alternating sides
  • particles: spawned at muzzle world position, oriented to image_angle, smoke side mirrored automatically
  • armor handled in End Step
  • search pattern: after losing FOV, it goes to lastKnown, then sweeps in a spiral-like pattern around it

oArmorPlate

Create Event

gml
event_inherited(); parentShip = noone; // Configurable sprite_index = -1; image_speed = 0; maxHp = 20; hp = maxHp; plateArmor = 2; plateRicochetMult = 10;

Step Event

gml
event_inherited(); if (!instance_exists(parentShip)) { instance_destroy(); exit; } x = parentShip.x; y = parentShip.y; image_angle = parentShip.image_angle;

Destroy Event

gml
event_inherited(); if (hp <= 0) { part_particles_burst(global.partSystem, x, y, psShipExplode); }

oEnemyL1

Create Event

gml
event_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;

Step Event

gml
event_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; } } */

End Step Event (armor aggregation + plate break → RAM trigger)

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

Alarm 11 Event (flare cooldown)

gml
canDeployFlares = true;

Destroy Event (optional safety)

gml
event_inherited(); /* if (instance_exists(dangerInst)) { instance_destroy(dangerInst); } */

Notes you may need to adjust (1-time)

1) Gas cloud object name

I used oGasCloud. If yours is different, replace it in Step:

gml
inGasCloud = place_meeting(x, y, oGasCloud);

2) Plate sprites

I referenced:

  • texL1PlateA
  • texL1PlateB

Swap to your real sprite names.


Next ship ideas (built on what we added)

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:

  • Ram tells: brief pre-ram slow/turn (readability)
  • Ram aftermath: short “engine stall” window where it can’t shoot (counterplay reward)
  • Broken-plate thrusters exposed: when plates are gone, it accelerates faster but takes more crit damage (risk/reward)

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

text
if (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:

  • the ship itself (target.armor)
  • or the closest armor plate belonging to that ship (oArmorPlate.parentShip == target) if one exists
    (so you get plate armor feedback instead of hull armor when plates are present)

It also:

  • uses the plate’s plateArmor if present (falls back to armor)
  • uses the plate’s circleRadius if it has one, otherwise uses the ship’s circleRadius
  • draws the armor icon and number at the right offset

✅ Modified Code (replace only the inner HUD block)

Replace this block:

gml
if (instance_exists(target) && point_distance(mouse_x, mouse_y, target.x, target.y) < 1500 && HUD_Lead) { ... }

with the version below:

gml
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); // ------------------------------------------------------------ // 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); }

Notes / Options

✅ If you want to show plate HP too (optional)

Since plates have hp, you can do:

gml
if (armorTarget != target && variable_instance_exists(armorTarget, "hp")) { draw_text(target.x - 1, target.y + offset + 30, string(armorTarget.hp)); }

✅ If you want to show “Hull armor when no plates left”

This already happens naturally because bestPlate won’t exist → armorTarget = target.

✅ If you want to show the plate nearest to the mouse instead of nearest to the player

Replace:

gml
var d = point_distance(other.x, other.y, x, y);

with:

gml
var 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:

  • Shows oArmorPlate.plateArmor
  • Shows oArmorPlate.hp
  • ✅ Falls back to ship hull armor only if no plates exist
  • ✅ Uses the closest armor plate belonging to the target
  • ✅ Keeps your existing penetration → color logic

I am not changing your structure, only extending it.


✅ Drop-in replacement for the ARMOR HUD section

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

✅ What the player sees now

When armor plates exist

text
[ ARMOR ICON ] 2 24 ← plate HP

When all plates are destroyed

[ ARMOR ICON ]  6

(HP line disappears automatically)


Optional polish (recommended)

🔹 Different icon for plate vs hull

If you want instant readability:

gml
var armorIcon = texIconArmor; if (armorTarget != target) { armorIcon = texIconArmorPlate; }

Then replace texIconArmor in draw_sprite_ext() with armorIcon.


Why this is correct for your architecture

  • ✔ Uses existing oArmorPlate instances
  • ✔ No assumptions about plate count
  • ✔ HUD always reflects what the bullet will hit first
  • ✔ Works with future ships reusing armor plates
  • ✔ Zero gameplay-side effects

If you want next:

  • color the HP text based on remaining %
  • show broken plate indicators
  • snap HUD to the specific plate sprite instead of ship center

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

text
if (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

思考

Modifying code for target selection and lead indicator

I need to adjust the selection process so that when choosing between “HUD_Lead” and “Wheel1,” it picks the closest target—either from hostiles or armor plates. The lead indicator should follow the selected target’s position and velocity, using the parent ship’s speed for plates. Also, I’ll update the display to show icons and health for plates and handle all target types in the selection.

Thought for 7s

回答

Here’s a drop-in modified version of your code that adds:

  • ✅ You can select oArmorPlate the same way as grpHostiles (Wheel1 selects nearest to mouse among both)
  • ✅ Lead indicator works for plates (uses parentShip velocity if available)
  • ✅ Plate HUD shows:
    • texIconArmorPlate
    • target.armor
    • target.hp
  • ✅ Hull HUD stays the same (texIconArmor, target.armor)
  • ✅ Works even if plate has no parentShip (falls back safely)

Assumptions:

  • oArmorPlate exists and has vars: armor, hp, and parentShip (optional but recommended).
  • Plates sit at ship x/y so lead uses parent ship speed to be accurate.

gml
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; // 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))); } } }

Small optional upgrade (if you want plates selectable only when “near” the ship)

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:

  1. pick nearest hostile ship
  2. then pick nearest plate whose parentShip == that ship

But 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:

  1. Pick nearest hostile ship (from grpHostiles) near the mouse
  2. If that ship has plates, pick the nearest plate belonging to that ship (via parentShip)
  3. If the mouse is closer to that plate than to the ship, select the plate, otherwise select the ship
  4. Lead indicator + HUD use plate (armor+hp + texIconArmorPlate) when plate is selected; otherwise hull (armor + texIconArmor)

Drop-in replacement for your whole block:

gml
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; // 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.

gml
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; // 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);
}

text
if (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:

  • draws the lead indicator for whichever was selected
  • shows armor for both
  • shows hp only for armor plates
  • uses texIconArmorPlate for plates, texIconArmor for ships
gml
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 (!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

思考

Thought for a couple of seconds

回答

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:

  • Prefer plate when dP <= dH (tie goes to plate)
  • Optional: add a tiny bias so plate wins even if it’s slightly farther due to rounding

✅ Replace your “choose nearest between grpHostiles and oArmorPlate” section with this

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 you want it even more deterministic

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:

gml
var 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)
{

text
if (!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)
{

text
if (!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); } } }

}

思考

Modifying ricochet behavior and updates

The script needs modifications based on the new ricochet requirements. For the bullet to not collide with the same object again, it should have an "ignore" flag, so the collision code checks this. For simple ricochet, I’ll calculate the new angle based on the bullet’s incoming direction and apply the updated approach.

Updating ricochet and damage calculation logic

For the ricochet angle, I'll use the formula bulletAngle + 180 + random_range(-90, 90) and normalize it. Regarding damage, if the bullet’s penetration is 3 levels below the target's armor, there shouldn’t be any damage, but it might still ricochet. I'll handle that and add particle effects when needed. For blastwave, I'll refine the damage formula based on distance and armor.

Thought for 14s

回答

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)


1) ApplyPlayerDamage (modified)

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

2) ApplyDamage (modified)

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

3) Blastwave (modified)

This keeps your “damage depends on armor”, but makes it consistent + safe for any object:

  • armor defaults to 1
  • damage always divides by armor
  • no weird reliance on other.armor inside with that might not exist
gml
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) { // 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)); } } } }

IMPORTANT: one tiny bullet-side change you must add

These scripts set:

  • bullet.ricochetIgnoreId
  • bullet.ricochetIgnoreTime

So 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):

gml
if (variable_instance_exists(id, "ricochetIgnoreTime") && ricochetIgnoreTime > 0) { if (other.id == ricochetIgnoreId) { exit; } }

And in the bullet Step event (or wherever):

gml
if (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);

text
draw_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();

text
var 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);

}

思考

Fixing tooltip drawing order and collisions

The issue is that the tooltip drawing happens before collision checks, so I need to move the tooltip drawing to after those checks. I'll make sure the FCS conditions stay intact while reordering code. I'll also include an armor plate mouse collision display with timing managed by alarms.

Adjusting alarm and coords for armor HUD

Since alarm 11 is already used for tooltip drawing, I'll create a separate alarm (12) to hide the armor plate HUD. I'll ensure proper coordinate conversion between GUI and room space, use mouseX/mouseY for accurate collisions, and keep the FCS condition intact by including it in that block.

Reordering code execution for tooltip rendering

To fix the tooltip order,

Thought for 31s

回答

Your textToDraw1/2/3 aren’t drawn because you draw them BEFORE you ever set them.

Right now the order is:

  1. if (!oPlayer.damagedFCS) { ... if (drawText) draw_text(textToDraw...) } ✅ runs early
  2. later you do mouse collision checks and set drawText = true; textToDraw... = ...; alarm_set(11,100); ✅ runs after
    → so the draw happens while drawText is still false / old strings.

Fix (keep all FCS conditions)

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).


1) Move the tooltip draw block (FCS preserved)

REMOVE this block from where it currently is (near the top of FCS):

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

ADD it near the end of the 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.


2) Add Armor Plate hover HUD at mouse for X time (Alarm)

You asked: only on mouse collision show armor plate icon + armor + hp near the mouse for a while.

Add these vars somewhere persistent (Create event of oCamera is best)

gml
showArmorPlateHud = false; armorPlateHudArmor = 1; armorPlateHudHp = 0; armorPlateHudColor = c_white;

Add this block inside your mouse collision section (in Draw GUI)

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

Draw the armor plate HUD (also in Draw GUI)

Place this somewhere after you set/reset draw settings (any time after you compute mouseX/mouseY is fine):

gml
if (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); }

Add Alarm 12 event to oCamera

gml
showArmorPlateHud = false;

Why your original “line from mouse to plate” didn’t work

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.

このQ&Aを共有