Help with a SpreadShot Feature

Question: My question is about how to get a functional spreadshot feature running in my game, run with a right click. Currently, I have a square, and when you left click, it shoots a another square, or bullet. I want it so that when I right click, it shoots in a spreadshot way. It only works some of the time though, and I can’t find out why. I have the basics of it done, but can’t seem to get the spreashot itself working. You can find it in the entities.mjs file, on line 154. You can see the parameters I’ve added. I plan to make it so I can use it in multiple different ways.

Repl link:

shotgunShoot(mouseX, mouseY, spread, damage, bulletSpeed, bulletSize, numberOfBullets) {
        // Getting Player Center, mouse click Coords
        let playerCenterX = this.x + 25;
        let playerCenterY = this.y + 25;

        // Redining Into Libraries for Ease of use in the code
        const player_pos = { x: playerCenterX, y: playerCenterY };
        const mouse_pos = { x: mouseX, y: mouseY };

        // Calculate the angle between player and mouse
        const angleToMouse = Math.atan2(mouse_pos.y - player_pos.y, mouse_pos.x - player_pos.x);

        // Calculate the angle increment for each bullet
        const angleIncrement = spread / numberOfBullets;

        console.log("Angle to Mouse:", angleToMouse);
        console.log("Angle Increment:", angleIncrement);

        if (isNaN(angleToMouse) || isNaN(angleIncrement)) {
            console.error("Invalid angle values. Aborting shotgunShoot.");

        // Loop through the specified number of bullets
        for (let i = 0; i < numberOfBullets; i++) {
            // Calculate the angle for the current bullet
            const angle = angleToMouse - spread / 2 + i * angleIncrement;

            console.log("Bullet Angle:", angle);

            // Making bullet Dimensions
            const bulletDimensions = {
                x: player_pos.x,
                y: player_pos.y,
                width: bulletSize,
                height: bulletSize,

            // Convert polar coordinates to Cartesian coordinates
            const bullet_vector = new Vector2(Math.cos(angle), Math.sin(angle));

            // Adding bullet to the bullet list
                rect: bulletDimensions,
                vector: bullet_vector,
                velocity: bulletSpeed,
                damage: this.utils.randint(damage.min, damage.max),

        // Updating last shot time
        this.lastShotgunTime =;

Do you have more details? Like, there’s a specific scenario where it stopped working?
For example, you can use the browser’s developer tools to step through the code in the shotgunShoot method to observe the flow of execution and inspect anything different. (So you might detect when the spreadshot doesn’t work)

It’s difficult to provide help with something as vague as “work sometimes” since it can be a lot of different things


Let me explain how it should work. It takes in the mouse position, number of bullets, and the spread. Other parameters, such as bullet speed and size, don’t matter too much. Then, it should find out the angle increment (numberOfBullets / spread). The angle increment is just how much it has to change the bullet destination. It will create the number of bullets at the new increment. It functions similarly to how a sprinkler works, except it creates them all at the same time.

In theory, this should work. However, I’ve run into a bug that seems to be related to how they are created. The problem that occurs also seems proportionate to the bullet spread angle. The bullets will instead shoot at different locations, and where and how depends on the angle. An example of this is when the spread is 90, and the numberOfBullets is 7-9. It will either shoot in a perfect spread (bullets spread over a 90-degree angle, evenly), or it will shoot at 5 separate points, like the points on a star. Any excess bullets will just be shot with the others; for example, if there were 8 bullets, and it shot in the star formation, 3 out of the 5 points would have 2 bullets, and not just 1. I can’t find a pattern as to when this happens; it appears to be random. I tried shooting in the same spot multiple times and couldn’t find any type of pattern or correlation between the mouse position or anything else. A second instance of this bug occurring is when I changed the spread to 30. It will sometimes shoot in that perfect 30-degree spread, and then it has two other possible instances that are unintended. First, it will shoot in 3 different evenly spaced spots (think a triangle’s points), and will shoot all of the bullets there. The second instance is it shoots in a complete circle, shooting all of the bullets evenly spaced apart, in a complete 360 spread. This also seemed to be completely random. I can provide more information if you need!

It’s starting to make more sense now. You still need narrow down the cause of the bug.

The thing is, when you use floating-point arithmetic, it can introduce small errors in calculations, more so when you use things like Math.atan2, Math.cos, etc. The amount of these small errors can accumulate and result in noticeable bugs when creating a larger number of bullets.

You can (temporarily) remove randomness from the number of bullets and use a fixed number to test the spreadshot feature. So your testes will be more consistent too.

Another thing is to review the angle calculation logic. Like, for n bullets, the typical calculation for the angle between bullets (angleIncrement) should be the total spread divided by n - 1.


// Calculate the angle increment for each bullet
const angleIncrement = spread / (numberOfBullets - 1);

And then you would apply it like this:

for (let i = 0; i < numberOfBullets; i++) {
    // Calculate the angle for the current bullet
    const angle = angleToMouse - (spread / 2) + (angleIncrement * i);
    // instantiate bullet

Just remember that, if numberOfBullets is 1, you should handle this as a special case to avoid division by zero (division by zero is undefined in mathematics and will result in an error in most (or all) programming languages).

And make sure that the final angle for each bullet is wrapped correctly within the 0-2π (or 0-360 degrees) range.


Just another thing if there is only one bullet to be fired, the concept of a “spread” doesn’t really apply because a single bullet can’t be spread out, it would just go in one direction. Therefore, when numberOfBullets is 1, you should not attempt to calculate angleIncrement at all. Instead, you should just fire the single bullet directly at the angle towards the mouse click.

For example:

if (numberOfBullets === 1) {
    // If there's only one bullet, fire it directly towards the mouse click
    const bullet_vector = new Vector2(Math.cos(angleToMouse), Math.sin(angleToMouse));
    // So you create and fire single bullet
} else {
    // Within the else, you calculate angle increment for multiple bullets
    const angleIncrement = spread / (numberOfBullets - 1);

    for (let i = 0; i < numberOfBullets; i++) {
        // Than you calculate the angle for the current bullet
        const angle = angleToMouse - (spread / 2) + (angleIncrement * i);
        // and instantiate each bullet

So, this way, you’re respecting the logic that a spread of bullets implies multiple directions, while a single bullet only has one direction. It also avoids the problematic division by zero.


First things first, it’s fixed! That was around a week of confusion for that. Anyway, thanks for proposing those ideas! I found that having a spread of 90 and 8 bullets made a perfect spread, but having pretty much anything else resulted in something random. I fixed this by converting the degrees into radians. I also had to change the code that created the bullet’s destination. You got me set on the right destination. The problem did end up being that it couldn’t convert the increment into something that could be used very easily. It now works with any angle, and any number of bullets. (I even tried 900 bullets, and I can’t say it ran smoothly, but it ran correctly)

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.