Lifesteal, a crit, anything that fires “every time it hits” needs to know that a unit attacked. StarCraft has no such event. What it has is the weapon cooldown: it jumps to its maximum the instant the unit fires and only ever counts down afterwards. There are two ways to use that, and they are different in character.
One fact first: StarCraft randomises every attack's cooldown by plus or minus 2. A Marine's 15 becomes 17, 16, 15, 14 or 13 - which is why a line of Marines goes brrrrt rather than firing in unison. So watching for “cooldown == 15” misses four shots in five.
// 마린은 쿨다운 15 → 12(0xC)를 봅니다 / Marine: cooldown 15, so watch 12
if (MemoryXEPD(unitEPD + 0x54/4, Exactly, 0xC00, 0xFF00)) {
// 이번 프레임에 쾏습니다 / it fired this frame
}One condition and no state to keep - far cheaper than watching hit points or scanning for projectiles. It has exactly one limit: a weapon with a cooldown of 3 or less has no such window.
The second way keeps the previous frame's cooldown per unit and fires on any increase. A cooldown only ever counts down on its own, so ANY rise is a shot - whatever the weapon, whatever the randomisation, however short the cooldown.
const prevCD = dwread_epd(extra + OFFSET_GCOOLDOWN);
if (unit.gCooldown > prevCD) { /* 쌀다 / it fired */ }
SetMemoryEPD(extra + OFFSET_GCOOLDOWN, SetTo, unit.gCooldown);Choosing between them is clear: the window when you have one weapon and care about cost, the rise when the map arms many different units or when a weapon is too fast for a window. The rise costs one stored value per unit.
Where to put that one value per unit is the next problem, and CUnit's genuinely free bytes run out fast. ex_cunit allocates a parallel block of extra fields, one per unit slot.
import ex_cunit; const MyCUnit = ex_cunit.ExtraCUnit(); const OFFSET_GCOOLDOWN = 0; const OFFSET_MANA = 1; const extra = MyCUnit.jump_epd(unitEPD); // 이 유닛의 추가 블록 dwread_epd(extra + OFFSET_GCOOLDOWN);
| Db(1700 * 336) | A parallel block exactly the size of the real CUnit table. That is the point: the distance from a unit's CUnit to its extra data is a constant, so the jump is one action. |
|---|---|
| 84 fields | 336 bytes is 84 four-byte fields, per unit. |
| You must initialise it yourself | StarCraft zeroes a CUnit when it hands out the slot; it has never heard of your parallel block. Clear it yourself in EUDLoopNewUnit() - otherwise the previous occupant's values are still sitting there. |
Once you know it fired, what it hit comes from two fields: 0x5C is the order's target UNIT and 0x58 is the order's target POSITION - one for “what did I hit” and one for “where did I click”.
const tPtr, tEpd = cunitepdread_epd(unitEPD + 0x5C/4); // 대상 유닛
const x, y = posread_epd(unitEPD + 0x28/4);
const tx, ty = posread_epd(tEpd + 0x28/4);
setloc("fx", x, y);
for (var i = 0; i < 12; i++) {
var dx, dy = lengthdir(64, atan2(ty - y, tx - x));
addloc("fx", dx, dy); // 대상 쪽으로 걸어감
/* 그리거나 피해를 줍니다 / draw or damage */
}Two things worth knowing from the same place. A unit that is out of ammunition has its groundWeaponCooldown pinned at 2 - check that first if a cooldown never counts down. And to freeze a value the engine increments every frame, do not overwrite it with SetTo: compare against what you want and subtract the difference. Overwriting races the engine's increment and jitters.
Damage and projectiles of your ownThree ways to read the struct wrong