Entry 170
"The enemies' weapon trails and flashes: ccEnemyWeaponCtrl"
Documented in Battle rules.
Filescrates/piney-battle/src/weapon.rscrates/piney-battle/src/prim.rscrates/piney-battle/src/enemy_motion.rscrates/piney-battle/src/lib.rscrates/piney-battle/examples/weapon_probe.rscrates/piney-battle/examples/battle_probe/enemy_motion.rscrates/piney-battle/examples/battle_probe/spawn.rscrates/piney-world/src/combat/weapon.rscrates/piney-world/src/combat/mod.rscrates/piney-world/src/field_world.rscrates/piney-game/src/session.rscrates/piney-game/src/session/tests/weapon.rstools/test_enemy_weapon_rs.pydocs/engine/battle.md
This was the last combat visual not yet ported. Each enemy race's
constructor makes a weapon controller over its type's ccEnemyWpInfo
rows. The race's exclusive() runs it every frame and its note() hands
it every note. The motion layer already raised the two calls, but nothing
ran them. So no enemy's swing left a trail, and no hit flashed.
battle.md also said the controller drew from the generators, so fights
drifted without it.
What the game does
- The source. gcmn 0x0043c600-0x0043ece8 holds
ccEnemyWeaponRad::ctrl,ccEnemyWeapon(constructor,init,setEdgeRate, the cells,setCell,interpolateCell,ctrlCell,dispCell,makePacketCell,ctrlRadiate,checkWeapon) andccEnemyWeaponCtrl(constructor,ctrl,note). Around it areeneSplineHandccPrimPacket'smakePacketandsendPacket. - A weapon. A weapon is up to four points (edges) on a node, with a
pool of
life x (between + 1)cells in a newest-to-oldest list:- While the enemy's attack is one the row lists (act 6 and bit
0x80 << atkNum), each frame adds a cell at the node's matrix. - Bezier or spline rows then put
betweencells between the second and third newest. - Every cell fades by
0.4 / lifea frame, and its points' alphas compound with it.
- While the enemy's attack is one the row lists (act 6 and bit
- The drawing.
dispCellsends the points on layer 6, edge pair by edge pair and newest cell first. There is a strip packet and a line packet (the lines 1.1 times as opaque).makePacketsets ADC where a run starts, where a point is behind the eye, and where both points are far off the screen. - The flash. A hit (note 0x8005 in attacks 0, 1, 4, 5) flashes weapon
paramfor 20 frames. A skill's start (0x8003 in attacks 2, 3) flashes every weapon for 80 frames, kept on the node. The flash is accPrimRadiateplate of 8 rays that faces the camera, whichccPrimRadiate::createturns bycameraGetRot2orcameraGetRot. It swells withsin, puts a light in the group, and goes out afterlife + 2frames. - No random draws. Nothing in the controller calls
rand()orccRand(). The flash's plate has nodpLengthordpBank, socreatePlatedraws nothing either. The note inbattle.mdthat fights drift without the controller was wrong for the weapons. - One quirk. A one-edge row divides 0 by 0 in
setEdgeRate. libgcc's soft float (built without NaNs) packs the result with an exponent__unpack_dnever set, so the share is whatever the stack held. The harness first failed there, on the 24th table: the same row gave 1.0019531 on a fresh stack and 0 after the other tables had run. Zeroing the stack before each build made them agree. One-edge rows draw no trail, so the value is never seen. - The tables. After main's merge every race names its tables: 97
tables, 225 rows. The V race's rows 271-273 name the animation's
EXT_objects. G's sword, club and rod (types 1-4) are four-edge spline rows on every attack. - The one chunk. 0x005db9c0's third row names
DMY_xdummy_w05.setCellturns the node's matrix by the chunk's turn. It also copiesApplyMatrix(m, chunk pos)to what looks like a spare vector but is the matrix's last row, so the flash sits at the chunk's place. The harness found this: the one failing table after the merge, w 0 where the port had 1.
What was built
piney_battle::weapon. The rules:WeaponCtrl::{new, ctrl, note},Weapon(the cells in an array, the list as indices) andrad_ctrl. They use the same FPU operation order as the game: the bezier through the accumulator,eneSplineH's lanes, the doubles ofsetEdgeRate.WeaponWorld. The world the rules run in: the node's matrix,ccTransPosFW2LWand the camera's turn. It sits over a newprim::RadWorld, which the entry control'sCxalso implements now.Radiate::creategained the camera-facing turn.- The two calls. They now carry
WeaponAt:dispSW,actNum,actCnt,atkNumand the skill's element. piney_world::combat::weapon. It builds a controller from the gcmn rows onentry::Out::Weaponand drops it onDestroyed. It runs the notes and frames in show order after the entry control. A node is the enemy's body posed at this frame'sCall::Drawmatrix. The trails go toCombat::trails, whichfield_world::draw_trailsdraws the waysendPacketdoes, with the lines one pixel wide. The flashes' rays join the boxes' rays.- The check.
tools/test_enemy_weapon_rs.pybuilds each tableraces.rsnames and a made-up one with the game's constructor in eemu, then runs 360 frames of notes andctrl. It compares every weapon, cell, flash andmakePacketcall againstweapon_probe: 98 tables, 1,188,443makePacketcalls, 20,882 flash frames and 6,036 notes, with 0 mismatches.GameBattle's no-opsceVu0CopyVectorhad to be taken off, andstrchrrun in Python. - The session.
goblin_swing_draws_a_trailplays event 3, then the east portal's goblin fighting Orca. Its sword swings once, and the trail is drawn for 18 frames, 21 pairs at its longest.weapon_shotssaves the frames with a cut-out close-up; an orange (fire) arc shows by the goblin.
Whether ccChar::Draw leaves the node coordinates stale on a frame it culls the body. The port uses the last drawn matrix with this frame's pose. The flashes' lights are not put in the scene's light group, like the boxes'. What the stream's decoded dummy chunk holds at +0x10 and +0x20 (the port takes the file's place with w 1, and its degrees as radians).