DEFENSE · TOWER ATTACK EVENTS

Assign Tower Fire by ID and Verify Every Attack Event

CURATED PATH

These links are maintained for a concrete prerequisite, failure mode, debugging step, or next lesson instead of generic topic similarity.

Defense / Combat

Recommended next steps

Switch defense priority to injured owned CreepsDefense priorityMove into Tower repair policy when no hostile remainsPeace-time policyVerify the previous tick's attack eventsRuntime evidenceCalculate range falloff, multi-Tower attack output, and Energy costOutput estimateDiagnosticsContinue diagnosis
Verification statusChinese source article: Reviewed in full · Official docs: Checked — attack(), Tower falloff constants, active Tower Power effects, Room.getEventLog(), EVENT_ATTACK and return codes · Diplomacy boundary: Allowed users and threat weights are project policies, not official alliance rules

VERIFICATION

Evidence and test status

Chinese source article
Reviewed in full
Official docs
Checked — attack(), Tower falloff constants, active Tower Power effects, Room.getEventLog(), EVENT_ATTACK and return codes
Diplomacy boundary
Allowed users and threat weights are project policies, not official alliance rules
Identity boundary
Every submitted intent stores the exact Tower ID and target ID
Execution boundary
OK accepts an attack intent; a matching prior-tick ranged attack event is required for verification
JavaScript syntax
Passed
Offline event review
Passed — Tower availability, diplomacy, threat ties, falloff estimates, Power modifiers, accepted intents, exact event matching and missed windows
Screeps Console test
Pending
Live Tower falloff, Power effect, TOUGH, healing, diplomacy and multi-Tower event test
Pending
Live multi-tick verification
Pending
Last verified
August 1, 2026

Use this guide when

Use this workflow when several Towers may attack in one tick and you need to prove which Tower fired at which target. A lower hit count is not enough evidence: hostile healing, TOUGH damage reduction, other Towers, Creeps, and target movement can all change the final state. The durable proof is a reviewed operation record followed by matching prior-tick EVENT_ATTACK entries for the exact Tower and target IDs.

Bind one volley to current IDs

function validateTowerAttackRequest(request) {
  if (!request || request.enabled !== true) {
    return { valid: false, reason: 'disabled' };
  }

  if (
    typeof request.requestId !== 'string'
    || request.requestId.length === 0
    || typeof request.roomName !== 'string'
  ) {
    return {
      valid: false,
      reason: 'request-invalid'
    };
  }

  return { valid: true, reason: 'valid' };
}

function getOwnedActiveTower(id) {
  const tower = Game.getObjectById(id);

  return tower
    && tower.structureType === STRUCTURE_TOWER
    && tower.my === true
    && tower.isActive() === true
      ? tower
      : null;
}

Store object IDs and plain values, not live objects. A Tower dispatcher should own the final action for each Tower during the tick.

Apply diplomacy before threat scoring

function getAttackableHostiles(room, allowedUsers) {
  const allowed = new Set(
    Array.isArray(allowedUsers)
      ? allowedUsers
      : []
  );

  return room.find(FIND_HOSTILE_CREEPS, {
    filter: creep =>
      typeof creep.owner?.username === 'string'
      && !allowed.has(creep.owner.username)
  });
}

function getTowerThreatScore(creep) {
  return (
    creep.getActiveBodyparts(ATTACK) * 5
    + creep.getActiveBodyparts(RANGED_ATTACK) * 5
    + creep.getActiveBodyparts(HEAL) * 4
    + creep.getActiveBodyparts(CLAIM) * 3
    + creep.getActiveBodyparts(WORK) * 2
  );
}

The allowed-user list and weights are project policies. They must be reviewed independently from the API's definition of a non-owned Creep.

Estimate range and active Power effects

function getTowerRangeAmount(basePower, range) {
  const clamped = Math.max(
    TOWER_OPTIMAL_RANGE,
    Math.min(TOWER_FALLOFF_RANGE, range)
  );
  const fraction = (
    clamped - TOWER_OPTIMAL_RANGE
  ) / (
    TOWER_FALLOFF_RANGE
    - TOWER_OPTIMAL_RANGE
  );

  return Math.floor(
    basePower * (1 - TOWER_FALLOFF * fraction)
  );
}

function getTowerEffectModifier(tower) {
  return (tower.effects || []).reduce(
    (modifier, effect) => {
      if (
        effect.ticksRemaining <= 0
        || (
          effect.effect !== PWR_OPERATE_TOWER
          && effect.effect !== PWR_DISRUPT_TOWER
        )
      ) {
        return modifier;
      }

      const values =
        POWER_INFO[effect.effect]?.effect;
      const factor = Array.isArray(values)
        ? values[effect.level - 1]
        : null;

      return Number.isFinite(factor)
        ? modifier * factor
        : modifier;
    },
    1
  );
}

function estimateTowerAttack(tower, target) {
  const range = tower.pos.getRangeTo(target);
  const rangeAmount = getTowerRangeAmount(
    TOWER_POWER_ATTACK,
    range
  );

  return {
    range,
    rangeAmount,
    estimatedRawDamage: Math.floor(
      rangeAmount * getTowerEffectModifier(tower)
    )
  };
}

This is an allocation estimate, not settlement evidence. The event log reports the actual damage event; target boosts and simultaneous actions can make net hits differ from the estimate.

Build one deterministic volley plan

function buildTowerAttackPlan(request) {
  const validation =
    validateTowerAttackRequest(request);
  if (!validation.valid) {
    return {
      ready: false,
      reason: validation.reason
    };
  }

  const room = Game.rooms[request.roomName];
  if (!room) {
    return {
      ready: false,
      reason: 'room-not-visible'
    };
  }

  const towers = room.find(FIND_MY_STRUCTURES, {
    filter: structure =>
      structure.structureType === STRUCTURE_TOWER
      && structure.isActive() === true
      && structure.store.getUsedCapacity(
        RESOURCE_ENERGY
      ) >= TOWER_ENERGY_COST
  });

  const hostiles = getAttackableHostiles(
    room,
    request.allowedUsers
  );
  if (towers.length === 0 || hostiles.length === 0) {
    return {
      ready: false,
      reason: towers.length === 0
        ? 'no-available-tower'
        : 'no-attack-target'
    };
  }

  const target = [...hostiles].sort(
    (left, right) =>
      getTowerThreatScore(right)
      - getTowerThreatScore(left)
      || Math.min(...towers.map(tower =>
        tower.pos.getRangeTo(left)
      ))
      - Math.min(...towers.map(tower =>
        tower.pos.getRangeTo(right)
      ))
      || left.id.localeCompare(right.id)
  )[0];

  return {
    ready: true,
    reason: 'ready',
    room,
    target,
    towers: [...towers].sort(
      (left, right) =>
        left.id.localeCompare(right.id)
    )
  };
}

The target is refreshed from current visible objects. The final ID tie-breaker is deterministic but has no combat meaning.

Submit one intent per Tower and save the contract

function submitTowerAttack(request) {
  const plan = buildTowerAttackPlan(request);

  request.lastCheckedAt = Game.time;
  request.lastStatus = plan.reason;

  if (!plan.ready) {
    return {
      status: 'not-submitted',
      reason: plan.reason
    };
  }

  request.enabled = false;
  request.submittedAt = Game.time;
  request.target = {
    id: plan.target.id,
    name: plan.target.name,
    owner: plan.target.owner?.username ?? null,
    hitsBefore: plan.target.hits
  };
  request.intents = plan.towers.map(tower => {
    const estimate = estimateTowerAttack(
      tower,
      plan.target
    );
    const energyBefore =
      tower.store.getUsedCapacity(
        RESOURCE_ENERGY
      );
    const result = tower.attack(plan.target);

    return {
      towerId: tower.id,
      targetId: plan.target.id,
      energyBefore,
      ...estimate,
      result
    };
  });
  request.status = request.intents.some(
    intent => intent.result === OK
  )
    ? 'accepted-pending-events'
    : 'failed-review-required';

  return {
    status: request.status,
    targetId: plan.target.id,
    intents: request.intents
  };
}

Disable before the calls so an old approval cannot repeat. Preserve every numeric return code; only accepted Tower IDs are eligible for event verification.

Match exact prior-tick attack events

function verifyTowerAttack(request) {
  if (
    !request
    || request.status !== 'accepted-pending-events'
  ) {
    return {
      status: 'submission-not-accepted'
    };
  }

  if (Game.time <= request.submittedAt) {
    return { status: 'pending-next-tick' };
  }

  if (Game.time !== request.submittedAt + 1) {
    return { status: 'event-window-missed' };
  }

  const room = Game.rooms[request.roomName];
  if (!room) {
    return { status: 'room-not-visible' };
  }

  const accepted = request.intents.filter(
    intent => intent.result === OK
  );
  const events = room.getEventLog().filter(
    event =>
      event.event === EVENT_ATTACK
      && event.data?.attackType
        === EVENT_ATTACK_TYPE_RANGED
  );
  const matches = accepted.map(intent => {
    const event = events.find(item =>
      item.objectId === intent.towerId
      && item.data?.targetId === intent.targetId
    );

    return {
      towerId: intent.towerId,
      targetId: intent.targetId,
      found: Boolean(event),
      damage: event?.data?.damage ?? null
    };
  });
  const matchedCount = matches.filter(
    item => item.found
  ).length;

  return {
    status: matchedCount === accepted.length
      ? 'verified-tower-volley'
      : matchedCount === 0
        ? 'tower-events-missing'
        : 'partial-event-match',
    submittedAt: request.submittedAt,
    verifiedAt: Game.time,
    matches
  };
}

Room.getEventLog() exposes events from the previous tick only. Verify at exactly submittedAt + 1; later checks cannot recover the missed event window.

Keep acceptance, events, and net hits separate

StatusMeaning
not-submittedThe room, Tower Energy, diplomacy filter, or target selection failed.
accepted-pending-eventsAt least one attack() call returned OK; previous-tick events are not available yet.
tower-events-missingNo accepted Tower ID produced the expected ranged attack event.
partial-event-matchOnly part of the accepted Tower set matched the target.
verified-tower-volleyEvery accepted Tower has a matching prior-tick attack event for the exact target ID.

Preserve documented return codes

Documented results are OK, ERR_NOT_OWNER, ERR_NOT_ENOUGH_ENERGY, ERR_INVALID_TARGET, and ERR_RCL_NOT_ENOUGH. Tower range changes damage; it does not add an ERR_NOT_IN_RANGE result.

Keep target policy and event proof distinct

This page owns one multi-Tower attack volley and its event identity. Diplomacy expiry, overkill allocation, hostile healing prediction, Rampart tactics, Safe Mode, and defender Creeps require separate policies. Use the Room event-log guide for a broader event consumer.

Official documentation

SOURCE AND SCOPE

Review the source, evidence, or next system

This English guide keeps the technical scope and verification boundaries of its Chinese source. The verification record shows which checks were completed and which runtime observations are still pending.