Tis

Minecraft 24w35a

This snapshot brings unexpected changes to raid spawning mechanics. See the changelog.

Salmon

Salmon variants now have different hitboxes sizes, fixing MC-275222.

Type Width Height
Small 0.35 0.2
Medium 0.7 0.4
Large 1.05 0.6

Raids

The way the game chooses where to spawn raiders has changed significantly. Some of these changes are meant to fix MC-275279.

24w34a Behavior

The game tries to find a valid spawn point during the 15 second (300 tick) countdown before a wave starts. Every 5 ticks (starting at 300 ticks, then 295, then 290…), the game attempts 3 times to pick a spawn location. The game will pick a different spawn location if the existing location is no longer in an entity-ticking chunk without waiting for the 5-tick cycle.

  • 300-100 ticks: picks a random location 64 blocks horizontally away from the raid center, then checks if the highest non-air block is within 64 blocks vertically of the raid center (known as spawning phase 1)
  • 300-100 ticks: picks a random location 32 blocks horizontally away from the raid center, then checks if the highest non-air block is within 32 blocks vertically of the raid center (known as spawning phase 1)
  • 39-1 ticks: picks the coordinate directly at the raid center (radius 0), but only if the highest non-air block is the raid center (known as spawning phase 3)
Ticks Phase Radius Bias Spawning Area
100-300 1 64 0-4 69x69
40-99 2 32 0-4 37x37
1-39 3 0 0-4 5x5

After the random coordinate is chosen and rounded down, the game selects a random bias from 0-4 in the X direction and a random bias from 0-4 in the Z direction. The bias is added to the random coordinate to create the spawn location (so the spawn location is biased in the +X and +Z directions).

If the raid countdown expires and there isn’t a valid spawn location yet, the game tries 20 attempts with spawning phase 1, 20 attempts with spawning phase 2, and 20 attempts with spawning phase 3.

24w35a Behavior

When the game tries to find a valid spawn point during the wave countdown, it makes 8 attempts every 5 ticks instead of 3 attempts every 5 ticks.

The game no longer separates the raid countdown into three spawning phases. Instead, each 20-tick interval has different radius and bias values (bias is randomly picked from the listed numbers):

Ticks Horizontal Radius Bias Spawning Area
300 97.92 0,3,6 105x105
280-299 90.88 0,2,4 96x96
260-279 83.84 0,2,4 89x89
240-259 76.80 0,2,4 82x82
220-239 69.76 0,2,4 75x75
200-219 62.72 0,1,2 66x66
180-199 55.68 0,1,2 59x59
160-179 48.64 0,1,2 52x52
140-159 41.60 0,1,2 45x45
120-139 34.56 0,1,2 38x38
100-119 27.52 0 29x29
80-99 20.48 0 22x22
60-79 13.44 0 15x15
40-59 6.40 0 8x8
20-39 0.64 -2,-1,0 4x4
0-19 7.68 -2,-1,0 11x11

Note that:

  • Horizontal radius ranges from 0.64 to 97.92.
  • Bias ranges from -2 to +6.
  • The radius decreases with time until it increases between 20-39 ticks and 0-19 ticks.

As an example (assuming the raid center is at (0, 0)), if the raid countdown is within 20-39 ticks, the radius is 0.64 and the bias is either -2, -1, or 0. Since the selected coordinate within the 0.64 block ring is rounded down to the nearest block, the possible coordinates are a 2x2 square: (0, 0), (-1, 0), (0, -1), and (-1, -1). Accounting for the -2 to 0 bias, mobs can spawn in a 4x4 square with the raid center in the +X+Z corner.

When the spawn location is selected, the highest non-air block now must be within 96 blocks (instead of 0, 32, or 64 blocks) vertically of the raid center. This applies to every wave, not just the starting wave.

If the raid countdown expires and there isn’t a valid spawn location yet, the game tries 5 times (instead of 3 times), 20 attempts each, to find a valid spawn location. The radius is always 7.68 and the bias is always from -2 to 0.

Previously, each attempt would select a completely random point on the spawning ring. Now, for each set of attempts (8 attempts during countdown and 20 attempts after countdown), the game chooses the point on a random starting angle, then tries increasing the angle in 1/16th increments. This causes the game to search a half-circle arc during each countdown attempt, and to search the full circle once the countdown has finished.

Code Comparison

The old behavior is equivalent to the following code:

public void tick() {
  if (!shouldTryToFindSpawnLocation()) {
    return;
  }

  if (raidCooldownTicks > 0) {

    // Tries the 3 spawning phases in order depending on time left
    int spawningPhase;
    if (raidCooldownTicks >= 100) {
      spawningPhase = 1; // 100-300 ticks
    } else if (raidCooldownTicks >= 40) {
      spawningPhase = 2; // 40-99 ticks
    } else {
      spawningPhase = 3; // 0-39 ticks
    }
    spawnLocation = findRaidSpawn(3, spawningPhase);

  } else {

    // When countdown expires, game tries all 3 spawning phases
    // one after the other as a last ditch effort
    for (int spawningPhase = 0; spawningPhase < 3; spawningPhase++) {
      if (spawnLocation != null) {
        spawnLocation = findRaidSpawn(20, spawningPhase);
      }
    }

  }
}

private BlockPos findRaidSpawn(int numAttempts, int spawningPhase) {
  for (int i = 0; i < numAttempts; i++) {
    // Random angle chosen per attempt
    float randomAngle = random.nextFloat() * (float) (Math.PI * 2);

    // Radius 64 for spawning phase 1,
    // 32 for phase 2,
    // 0 for phase 3
    int spawnRingRadius = 64 - 32 * (spawningPhase - 1);
    // Biases from 0-4 are chosen separately for X and Z directions
    int biasX = random.nextInt(5);
    int biasZ = random.nextInt(5);

    // Spawn locations are floored to nearest block position
    int spawnX = center.getX() + (int) Math.floor(Math.cos(randomAngle) * spawnRingRadius + biasX);
    int spawnZ = center.getZ() + (int) Math.floor(Math.sin(randomAngle) * spawnRingRadius + biasZ);

    // The highest block in the column must be within a
    // certain vertical distance from the raid center:
    // 64 blocks (phase 1)
    // 32 blocks (phase 2)
    // 0 blocks (phase 3)
    int spawnY = getHighestNonAirBlockY(spawnX, spawnZ);
    if (Math.abs(center.getY() - spawnY) <= spawnRingRadius) {
      return new BlockPos(spawnX, spawnY, spawnZ);
    }

  }
  return null;
}

The new behavior is equivalent to the following code:

public void tick() {
  if (!shouldTryToFindSpawnLocation()) {
    return;
  }

  // No more separation into spawning "phases"
  if (raidCooldownTicks > 0) {
    // During countdown, 8 attempts every 5 ticks
    // (unless existing position unloads)
    spawnLocation = findRaidSpawn(8);
  } else {
    // Once countdown expires, game does 100 attempts total
    // Unlike before, the ring radius and bias does not change
    // Radius is always 7.68 and bias is always -2, -1, or 0
    for (int i = 0; i < 5; i++) {
      if (spawnLocation != null) {
        spawnLocation = findRaidSpawn(20);
      }
    }
  }
}

private BlockPos findRaidSpawn(int numAttempts) {
  int raidCooldownSeconds = raidCooldownTicks / 20;

  // Now chooses a starting angle, then increases the angle
  // for each attempt in 1/16th increments
  // During countdown, sweeps through a half-circle
  // When countdown expires, sweeps through a circle and a quarter
  float randomStartAngle = random.nextFloat() * (float) (Math.PI * 2);
  for (int i = 0; i < numAttempts; i++) {
    float currentAngle = randomStartAngle + i * (float) (Math.PI / 8);

    // See table for radius and bias values
    float spawnRingRadius = Math.abs(7.04f * (float) raidCooldownSeconds - 7.68f);
    int biasX = random.nextInt(3) * (int) Math.floor(0.22f * (float) raidCooldownSeconds - 0.24f);
    int biasZ = random.nextInt(3) * (int) Math.floor(0.22f * (float) raidCooldownSeconds - 0.24f);

    // Spawn locations are still floored to nearest block position
    int spawnX = center.getX() + (int) Math.floor(Math.cos(currentAngle) * spawnRingRadius + biasX);
    int spawnZ = center.getZ() + (int) Math.floor(Math.sin(currentAngle) * spawnRingRadius + biasZ);

    // The highest block in the column must be within
    // 96 blocks vertically of the raid center,
    // no longer a variable radius based on spawning phase
    int spawnY = getHighestNonAirBlockY(spawnX, spawnZ);
    if (Math.abs(center.getY() - spawnY) <= 96) {
      return new BlockPos(spawnX, spawnY, spawnZ);
    }
  }
  return null;
}

Both behaviors share the following code:

private int raidCooldownTicks; // decrements from 300 to 0
private BlockPos center; // raid center
private BlockPos spawnLocation; // location raiders will be spawned, may be null

private boolean shouldTryToFindSpawnLocation() {
  if (spawnLocation == null) {
    // Try to find spawn location every 5 ticks...
    return raidCooldownTicks % 5 == 0;
  } else {
    // ...unless spawn location is no longer entity-ticking
    return !isEntityTicking(spawnLocation);
  }
}

Edits

September 7 2024

  • Clarified bias selection for new raid behavior
  • Extracted common code and explained local variables in code comparison section