Aller au contenu principal

Session 122 — Correction sémantique GLFT_AlienAnims_l (byte 0 vs byte 1)

Problème observé

--test-level ALIEN_ASHNARG affichait un robot armé (sprite robotright)
au lieu de l’alien organique attendu. Animation d’attaque produisait un
« spam droite/gauche » : le sprite alternait entre une pose et son miroir
non appliqué, donnant un effet de saute.

Cause racine

Confusion sémantique sur les bytes 0 et 1 d’une frame d’animation dans
GLFT_AlienAnims_l. La session 119 (idleFrame) avait lu byte 0
en le supposant être le numéro de frame PNG. C’est faux.

Vérification dans definitions.json actuel :
– Ashnarg (alien 4) option 0 : [[6,1,0,0],[6,1,0,0],[6,2,0,0]...]
– Red Alien (alien 0) option 0 : [[0,1,0,0],[0,2,0,0]...]
– Guard (alien 2) option 0 : [[11,1,24,0],[11,2,0,0]...]
– AlienPriest (alien 11) option 0 : [[12,1,0,0]...]
– Triclaw (alien 13) option 0 : [[3,1,0,0]...]

Et la table wadFiles[] :
– 0=ALIEN2, 3=TRICLAW, 6=ASHNARG, 11=GUARD, 12=PRIEST, 13=INSECT

Sémantique correcte vérifiée contre hires.s::JUMPALIENANIM :
byte 0 = gfxIndex (= index dans wadFiles[] = GLFT_ObjGfxNames_l)
→ détermine le WAD à charger pour cette frame.
→ valeur signed négative (typiquement 255) = end-of-anim marker (loop).
byte 1 = numéro de frame PNG signed dans le WAD.
→ magnitude = _fN.png.
→ valeur négative = miroir horizontal.

Le ASM original n’utilise PAS gfxType pour choisir le WAD. Chaque
frame déclare son propre WAD via byte 0. Le gfxType
(BITMAP/VECTOR/lightsourced) sert uniquement à choisir le code de rendu
(sprite plat vs modèle vectoriel vs glare).

Pour Ashnarg (defIdx=4), l’ancien code lisait byte 0 = 6, l’utilisait comme
frame number dans le WAD robotright (sélectionné par gfxType=2),
donnant robotright_f6.png au lieu de ashnarg_f1.png.

Le « spam droite/gauche » venait du fait que la table d’anim alterne régulièrement
byte 1 positif et négatif (= même frame, miroirée ou non) pour donner du
mouvement avec moins de PNG. Sans appliquer le miroir, on affichait deux
poses différentes au lieu de la même mirrorée.

Modifications

LnkParser.AlienAnimFrame :
– Ajout des getters gfxIndex(), signedFrame(), frameNumber() avec la
bonne sémantique. Les champs record gardent leurs noms historiques pour
compat API.
isAnimEnd() corrigé : utilise byte 0 signed < 0 (= règle ASM
tst.b ; bge .noendanim) au lieu de l’ancienne condition tout-zero
qui pouvait considérer la frame 0 d’un alien comme end-marker.
getAlienIdleFrame(idx) retourne maintenant frameNumber() (= |byte 1|).
– Nouvelle méthode getAlienIdleGfxIndex(idx) retournant byte 0.

LevelJsonExporter.exportDefinitions() :
– Ajoute le champ idleGfxIndex à chaque alien dans la section aliens[].

LevelSceneBuilder :
record AlienDef étendu avec idleGfxIndex.
– Nouveau champ wadFiles chargé par loadWadFiles() qui lit la table
wadFiles[] de definitions.json.
addItems() : pour les aliens, le WAD est résolu via
wadFiles[idleGfxIndex] (= équivalent fidèle du GLFT_ObjGfxNames_l[gfxIdx]
de l’ASM). Fallback en cascade sur ALIEN_WAD_BY_GFXTYPE puis "alien2"
si la résolution échoue.

AlienAnimsResolver.AlienAnimFrame :
– Mêmes ajouts que LnkParser.AlienAnimFrame (getters sémantiques + règle
isAnimEnd corrigée).

AlienSpriteController.update() :
– Refactorisé : lit frameNumber() (= |byte 1|) au lieu de wadFrame()
(= byte 0) pour le PNG à afficher.
– Nouvelle méthode applyFlip() qui applique le miroir horizontal en
modifiant les UV du quad (swap des U). Appelée quand isFlipped() change.
Fixe définitivement le « spam droite/gauche » en attaque.

Fix complémentaire — Spasmes intermittents (cycle de timer2 corrompu)

Après le fix initial, des spasmes intermittents persistaient (sprite qui
saute aléatoirement entre 2 poses). En épluchant hires.s::JUMPALIENANIM
et DOALLANIMS, j’ai découvert le modèle exact :

DOALLANIMS:
    subq.b  #1,thistime    ; décrémente compteur global
    ble.s   .okdosome      ; si <= 0, c'est le tour
    rts                    ; sinon, RIEN
.okdosome:
    move.b  #5,thistime    ; reset à 5
    ; ... pour chaque alien : addq #1,Timer2 (avec test bge end-of-anim)

L’animation alien avance EXACTEMENT 1 frame TOUS LES 5 ticks Amiga, peu
importe le mode (walk, attack, hit, die). Et EntT_Timer2_w est l’index
de frame DIRECT
— pas un compteur cumulatif.

Problèmes du code Java avant ce fix :
1. AlienAI.doDefault/doResponse/doFollowup/doTakeDamage faisait
a.timer2 += world.tempFrames(); — incrément par paliers irréguliers
(1 ou 2 selon scheduling JME).
2. AlienSpriteController faisait (timer2 / 8) % animLen — double
division qui amplifie les jumps.
3. Résultat : timer2 saute parfois de 8 à 24 entre deux frames de rendu,
provoquant un saut de 2 frames d’anim d’un coup. Si l’une est flippée
et l’autre pas → spasme visible.

Modifications additionnelles :

AlienRuntimeState :
– Nouveau champ animTickCounter (= thistime ASM, init à 5).
– Constante ANIM_TICKS_PER_FRAME = 5.
initFromDef initialise animTickCounter au max.

AlienAI :
Retire tous les a.timer2 += frames des handlers (doDefault,
doResponse, doFollowup, doTakeDamage, doDie).
– Toutes les transitions de mode (DEFAULT→RESPONSE, RESPONSE→FOLLOWUP,
TAKE_DAMAGE→DEFAULT, justDied) reset animTickCounter au max en plus
de timer2 = 0.
– Détection du tir (fireTrigger) passée de prevTimer2 < FIRE_FRAME &&
timer2 >= FIRE_FRAME
à timer2 == FIRE_FRAME && prevTimer2 != FIRE_FRAME
pour s’aligner sur la sémantique d’index de frame discret.

AlienSpriteController :
– Nouvelle méthode tickAnimation(state, cameraAngle) qui décrémente
animTickCounter ; quand 0 → reset à 5 et avance timer2 d’1 (avec
test ASM-fidèle tst.b ; bge sur la frame N+1).
update() ne fait plus aucune avancement de timer — lit juste l’index
courant state.timer2 et affiche la frame correspondante.
– Les anciennes constantes TICKS_PER_FRAME_WALK = 8 et TICKS_PER_FRAME_ACTION = 5
sont remplacées par une constante unique TICKS_PER_FRAME = 5 (l’ASM ne
distingue pas).
– Correction additionnelle : la frame PNG affichée est |byte1| - 1 et non
|byte1| directement (l’ASM fait sub.w #1,d0 à la ligne 2454 de
hires.s). Si byte1 = 1 → _f0.png, byte1 = 5 → _f4.png.

AlienControlSystem.update() :
– Avant la boucle d’IA, on appelle ctrl.tickAnimation() exactement
amigaFrames fois par alien. C’est l’équivalent direct du dosomething
ASM appelé 1 fois par frame Amiga (qui décrémente thistime).
– L’IA tourne ensuite normalement, sans toucher à timer2 (à part les
resets aux transitions).

Pipeline obligatoire pour tester

./gradlew convertLevels    # régènere definitions.json avec idleGfxIndex
./gradlew buildScenes      # rebuild scenes avec wadFiles[gfxIdx]
./gradlew run --args="--test-level ALIEN_ASHNARG"

Vérifications visuelles attendues

Test Avant 122 Après 122
ALIEN_RED_ALIEN alien2 (correct) alien2 ✓
ALIEN_GUARD guard (correct) guard ✓
ALIEN_ASHNARG robotright_f6 ✗ ashnarg_f1 ✓
ALIEN_ALIEN_PRIEST robotright ✗ priest ✓
ALIEN_TRICLAW guard ✗ triclaw ✓
ALIEN_BIG_INSECT robotright ✗ insect ✓

Attaques : plus de « spam droite/gauche » grâce au miroir UV.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *