Pathfinding par flow field
Les zombies ne suivent pas un A* classique calculé par entité : un flow field (carte de coût partagée) est recalculé pour toute une dimension, et chacun lit localement la direction descendante. Le champ encode aussi s'il faut casser un bloc ou construire un pont, et il ferme les colonnes où l'eau monte jusqu'au plan des joueurs.
Recalcul et étendue
Le champ est recalculé tous les 10 ticks (0.5s), hors thread serveur (pool dédié), sur une grille bornée à 192 blocs plus 24 blocs de marge autour des sujets sous surveillance.
GPU avec repli CPU
Si OpenCL est disponible, le GPU est utilisé automatiquement (useGpu
est un kill-switch : passer à false force le CPU, true ne force pas le GPU s'il n'y
en a pas). Toute erreur GPU retombe silencieusement sur le CPU. Il n'existe aucun seuil de taille : un GPU
présent prend tous les calculs, quelle que soit la grille. De toute façon, le plus petit champ que le jeu
construise fait déjà 49×49 (un joueur seul plus la marge de 24 blocs par défaut).
Le CPU utilise un Bellman-Ford parallèle multi-cœurs (cœurs disponibles − 2 threads), pas un Dijkstra mono-cœur : un serveur dédié headless sans GPU tourne donc identiquement, juste sur plusieurs cœurs plutôt qu'un seul.
Le GPU et le CPU sont conçus pour rester numériquement identiques : mêmes poids d'arête (orthogonal/diagonal, configurables) et mêmes pénalités casser/construire (le kernel OpenCL lit le même tableau de coût que le solveur CPU). Activer ou désactiver le GPU ne devrait donc pas modifier les trajets empruntés.
Casse de blocs taille-aware
La hauteur de casse pour avancer est calculée depuis la vraie hauteur de la boîte de collision du zombie (arrondi supérieur, plancher 1, plafond 4 blocs) : un corps agrandi par la variation aléatoire casse donc plus haut qu'un gabarit normal.
À portée de bras, les mains s'arrêtent
Avant de choisir entre grimper, descendre et casser, le pilote pose une question plus simple :
la cible est-elle déjà atteignable. Trois mesures y répondent ensemble. Moins de
meleeStopRange blocs à l'horizontale entre les deux centres, pas plus de
meleeStopHeight bloc d'écart vertical, et une ligne de vue que le jeu accorde.
Il en manque une, la réponse est non et l'activation continue son chemin.
Quand la réponse est oui, tout s'arrête là : le zombie mord comme n'importe quel monstre du jeu de base, sans casser un bloc, sans poser un pont, sans démarrer un pilier. La question est posée avant même le tri par niveau de détail, donc elle vaut pour le plus lointain des zombies suivis comme pour celui qui est sur toi.
C'est ce qui évite le spectacle d'un mur qui tombe derrière un joueur déjà au contact. Sans ce test, un zombie coincé contre un coin de la pièce pendant qu'il frappe compterait ses activations sans progrès, se déclarerait bloqué et se mettrait à creuser, alors que sa cible est à un bras. Les deux valeurs s'abaissent jusqu'à 0, ce qui rend le test impossible à satisfaire et rend aux zombies leur pioche à bout portant.
Une seule brèche par groupe
Dix zombies contre un mur, chacun tapant son propre bloc de devant, c'est dix blocs à 10 % et aucun trou. Le coordinateur de brèche existe pour ça. Il y en a un par dimension, et tout zombie sur le point de casser lui soumet trois choses : la colonne qu'il a devant lui, la case d'où il attaque, et la cible de chasse que cette brèche sert.
La réponse suit quatre cas. Un zombie déjà engagé sur sa colonne la garde, et l'inscrit
comme point de ralliement plutôt que de se laisser tirer vers celle du voisin en plein travail. Sinon le
coordinateur cherche les brèches ouvertes à moins de breachRadius blocs qui
servent à peu près la même cible, deux rayons de tolérance sur la cible elle-même pour que deux murs sans
rapport ne fusionnent pas. Si la plus proche est justement sa colonne, il la rafraîchit. Et si le groupe
tient déjà ses maxConcurrentBreaches brèches, il adopte la plus proche : l'appelant
le réoriente vers la case d'approche du premier casseur, si bien que la foule s'entonne sur un seul trou.
Reste le cas où il y a de la place, et il ouvre la sienne.
Une brèche que personne ne travaille pendant breachGraceTicks est
oubliée, ce qui libère le créneau, et celle dont la colonne ne bloque plus le passage disparaît aussi : le
trou est fait, il n'y a plus rien à coordonner.
La concentration paie en vitesse. Le casseur compte les zombies distincts qui ont demandé ce
bloc dans la fenêtre de grâce, et multiplie la progression par
1 + (casseurs − 1) × breakConcentrationPerBreaker, plafonné par
breakConcentrationCap. Quatre zombies sur la même pierre vont 2,8 fois plus vite
qu'un seul ; le plafond par défaut est atteint à six, et une horde entière n'ira jamais au-delà de quatre
fois. Une porte en bois cède en quelques secondes, une pierre tient, et c'est toujours le même mur qui
s'ouvre, pas dix éraflures côte à côte.
| Champ | Description | Défaut |
|---|---|---|
| meleeStopRange | Distance horizontale sous laquelle la cible compte comme atteignable, ligne de vue comprise. À 0, plus aucun arrêt : les zombies creusent au contact. | 2.0 |
| meleeStopHeight | Écart vertical maximal pour que cet arrêt s'applique. Au-delà, la cible est en hauteur ou en contrebas, donc les manoeuvres de bloc reprennent. | 1.5 |
| breachEnabled | Interrupteur de la coordination. À false, chacun casse sa propre colonne, comme avant. | true |
| breachRadius | Rayon dans lequel une brèche existante est adoptée au lieu d'en ouvrir une autre. Plus large, plus la foule s'entonne loin. | 6.0 |
| maxConcurrentBreaches | Brèches ouvertes en même temps dans un groupe. 1 impose strictement un bloc à la fois. | 1 |
| breachGraceTicks | Ticks sans travail avant qu'une brèche soit oubliée et son créneau rendu. | 20 (1s) |
| breakConcentrationPerBreaker | Gain de vitesse par casseur supplémentaire sur le même bloc. À 0, la vitesse ne dépend plus du nombre. | 0.6 |
| breakConcentrationCap | Plafond du multiplicateur, pour qu'un tas de zombies ne pulvérise pas la pierre. | 4.0 |
Escalade et descente
Escalade : impulsion de vitesse verticale réelle + pose d'un bloc support à chaque palier, jamais de téléportation. Trois conditions, pas deux : la cible doit être à au moins 2 blocs de hauteur et au plus 5 blocs à l'horizontale, le zombie doit être bloqué (deux activations consécutives sans progresser vers elle) et il doit être suffisamment proche pour être piloté finement. Un zombie qui se rapproche encore continue au sol. Plafonné à 24 blocs de hauteur totale. Le pilier pose toujours des blocs pour grimper (il se tient sur ce qu'il construit) ; il n'existe aucune variante d'escalade sans pose de bloc.
Descente : par ordre de préférence, marche ou chute courte sûre (jusqu'à 3 blocs) → escalier déjà taillé dans le terrain → puits vertical sûr → construction d'un escalier au-dessus du vide en dernier recours. En règle générale, il ne casse pas le dernier bloc surplombant un vide profond. Une exception : si la cible est à moins de 1.5 bloc à l'horizontale (donc quasiment sous ses pieds), il creuse quand même droit vers le bas pour la suivre dans le trou plutôt que de rester planté au-dessus.
L'eau qui barre la route
Le classifieur de cellules ouvre sur une question d'eau, avant même de chercher un appui
dans la colonne. Il lit waterAvoidDepth blocs empilés à partir du plan de
référence : deux par défaut, réglable de 1 à 8. Si tous sont de l'eau, source ou courant, la case devient
infranchissable, et le solveur la traite dès lors comme un mur plein, à contourner comme n'importe quelle
pierre. Un gué d'un seul bloc, lui, garde son coût ordinaire.
La lave n'entre pas dans ce test : posée sur un sol dur, sa colonne reste passable au tarif normal.
Le plan de référence est la hauteur moyenne des joueurs que le mod peut prendre pour cible, arrondie au bloc où sont leurs pieds. Une seule tranche par dimension, partagée par toute la horde, et sans joueur ciblable il n'y a pas de champ du tout : un zombie lancé sur un villageois entre dans l'eau sans le moindre détour.
Ces deux blocs sont un chiffre livré, pas la taille de celui qui lit la case, et un champ calculé une fois pour toute la dimension ne peut de toute façon pas connaître la stature de chacun. Un grand gabarit se voit donc interdire une eau de deux blocs qu'il aurait passée la tête au sec.
Le détour a un angle mort, et il est large. Les deux cases lues sont celle des pieds des
joueurs et celle juste au-dessus. Un étang à ras de sa berge a sa surface un bloc plus bas : les deux cases
testées sont de l'air, la colonne repasse par les règles ordinaires, la recherche d'appui descend jusqu'à
4 blocs (flowVerticalTolerance) et trouve le fond. La route traverse le plan d'eau
au lieu d'en faire le tour. Pour qu'un lac soit contourné, il faut que sa surface monte au niveau où les joueurs
se tiennent : une cave inondée, un bassin retenu sur une hauteur, un joueur posté sur une île au
milieu.
Rien de tout cela ne passe par le pathfinder du jeu. PathType et le
malus de fluide restent tels quels, setCanFloat n'est jamais appelé. Un malus vaut
pour l'entité entière et refuse un gué aussi sûrement qu'un lac, alors que la profondeur appartient au lieu et
se juge case par case.
Le champ est relu à chaque réémission de trajet, quelle que soit la distance. Quand cette lecture échoue, la navigation repart en ligne droite vers la cible. Cela arrive faute de champ actif, parce que le zombie est sorti de la grille ou posé sur une case infranchissable, parce que sa case ne porte pas de direction, ou parce que la navigation du jeu ne sait pas atteindre le point de passage.
Le zombie déjà dans le lac est dans le deuxième cas : sa propre case est celle que le classifieur a fermée. Mais dès que sa tête passe sous l'eau il quitte la logique de trajet avant même de la consulter, et repart vers la berge, ce que décrit Mood, fuite et survie. Quand aucun contour n'existe, la horde n'attend pas au bord : le champ n'ayant plus rien à lui proposer, elle marche droit et se noie.
Un effet de bord vaut d'être connu. Le test de l'eau passe avant la branche du mur cassable, donc deux blocs immergés empilés au plan de référence, escaliers, dalles ou barrières, font contourner une barrière que le même zombie aurait cassée au sec.
| Champ | Description | Défaut |
|---|---|---|
| cannotSwim | Interrupteur des deux moitiés : l'eau profonde devient un mur pour le champ, et un zombie la tête sous l'eau se noie. À false, l'eau redevient ordinaire pour tout le monde. | true |
| waterAvoidDepth | Nombre de blocs d'eau empilés à partir du plan de référence qui rendent la colonne infranchissable (borné de 1 à 8). | 2 |
| drownGraceTicks | Air disponible avant les premiers dégâts de noyade (borné de 1 à 280, soit 14 s au maximum). | 40 (2s) |
| drownDamage | Cran de dégâts ajouté à chaque activation passée le sursis. À 0, le zombie reste sous l'eau indéfiniment sans jamais mourir. | 4.0 |
| floatInWater | Nage en surface, le comportement inverse. Neutralisé tant que cannotSwim est actif, avec tout son bloc de réglages de vitesse et de plongée. | true |
Ces cinq champs se règlent en jeu comme les autres, par
les trois interfaces. Éteindre
cannotSwim en cours de partie rend la nage à tout zombie qui a une cible, dès son
activation suivante, parce que le pilote du mod relit l'option à chaque passage ; celui qui n'a pas de cible
reste sur le pilotage du jeu. Ce qui ne revient qu'aux zombies apparus ensuite, c'est le ballottement de
surface du jeu de base, accroché une seule fois à la création de l'entité. Dans le fichier JSON,
waterAvoidDepth rejoint floatInWater et le bloc
water* sous Water ; les trois noms qui ne portent ni water ni float tombent dans
Misc.
Le bond et la prise
Un zombie qui poursuit une cible peut se jeter dessus au lieu de la rejoindre au pas. La
tentative n'a lieu qu'une activation sur leapCooldownActivations, et seulement si
le tirage passe leapChance. La cible doit être dans la fenêtre
leapMinRange à leapMaxRange à l'horizontale, et pas plus
de leapMaxVerticalDiff blocs au-dessus ou en dessous. Un bond trop court se
terminerait par un pas ordinaire, un bond trop long tomberait à côté.
Avant de partir, le zombie sonde le sol devant lui, leapLandingScanDist
blocs en avant et leapLandingScanDepth blocs sous ses pieds. S'il n'y trouve rien de
dur, il ne saute pas. C'est ce qui l'empêche de se lancer dans un ravin pour rien, sans lui interdire le
saut au-dessus d'une tranchée d'un bloc.
La suite est nouvelle. Quand un bond se termine sur sa proie plutôt qu'à côté, le zombie
s'accroche. Il cesse de naviguer, se pose à l'endroit qu'occuperait un passager, et mord pendant une durée
tirée entre leapClingMinSeconds et leapClingMaxSeconds.
La prise vaut leapClingDamage point de vie par seconde, ce qui est peu par choix :
la plus longue coûte cinq points sur les vingt d'un joueur sans armure, et une morsure ordinaire fait plus
mal. Elle sert à immobiliser, pas à achever.
Ce qui compte comme un atterrissage sur la proie tient à deux mesures. La distance
horizontale entre les deux centres doit rester sous leapClingReachSq, exprimée au
carré pour éviter une racine par test, et le zombie ne peut pas être plus de
leapClingMaxDropBelow bloc sous elle. Il est censé retomber dessus, pas tendre le
bras vers le haut depuis le sol.
Une fois accroché, le zombie est reposé à chaque tick sur la tête de sa proie. Si le plafond ne laisse pas la place à un corps de 1,95 bloc, il se glisse dans l'empreinte de la proie elle-même, au niveau de ses pieds, ce qui évite d'avoir les yeux dans la roche et d'étouffer à raison d'un point par tick. Quand aucune des deux places n'est libre, il lâche. Il lâche aussi si sa victime meurt, disparaît ou change de dimension, et une sauvegarde qui tombe pendant la morsure ne laisse rien derrière elle.
Les dégâts sont mis de côté avant d'être remis. Le jeu de base avale toute blessure qui ne dépasse pas la précédente dans les dix ticks qui suivent, donc un quart de coeur régulier ne serait compté qu'une fois sur deux. Le mod accumule ce qu'il doit et le donne d'un coup dès que la fenêtre se rouvre. La poussée qui accompagne normalement un coup de monstre est annulée au passage : le jeu répond à une direction nulle par une direction au hasard et un peu de hauteur, ce qui ferait sauter la proie deux fois par seconde sous un zombie censé la tenir.
Le même zombie ne peut pas recommencer avant
leapClingCooldownActivations activations, six secondes au rythme par défaut, soit
plus long que la plus longue prise. Un zombie seul ne cloue donc jamais un joueur indéfiniment. À plusieurs,
c'est une autre histoire.
Côté image, un zombie accroché n'est pas debout : les bras se referment vers le bas, les genoux remontent, la tête est penchée dans la proie et hoche au rythme des morsures. Deux ondes de vitesses différentes portent ce mouvement, l'une pour le hochement et l'autre pour le roulis, si bien que la boucle ne se voit pas sur cinq secondes. L'information vient du serveur par un seul booléen synchronisé, et deux zombies accrochés au même moment ne sont jamais en phase, chacun comptant depuis sa propre naissance.
| Champ | Description | Défaut |
|---|---|---|
| leapEnabled | Interrupteur du bond. À false, les zombies rejoignent toujours leur cible au pas, et la prise ne peut plus se déclencher non plus. | true |
| leapCooldownActivations | Activations entre deux tentatives, pour un zombie donné. | 12 |
| leapChance | Probabilité de sauter sur une activation éligible. | 0.4 |
| leapMinRange | Distance horizontale en dessous de laquelle le zombie est déjà trop près pour sauter. | 2.5 |
| leapMaxRange | Distance horizontale au-delà de laquelle le bond tomberait court. | 8.0 |
| leapHorizontalSpeed | Vitesse horizontale imprimée au départ. | 0.55 |
| leapUpward | Impulsion verticale du départ. | 0.42 |
| leapMaxVerticalDiff | Écart vertical maximal, en valeur absolue, pour que le bond parte. | 3.0 |
| leapLandingScanDist | Blocs sondés en avant à la recherche d'un sol avant de s'engager. | 3 |
| leapLandingScanDepth | Blocs sondés vers le bas par ce même test. | 3 |
| Champ | Description | Défaut |
|---|---|---|
| leapClingEnabled | Interrupteur de la prise. À false, un bond qui touche sa cible rebondit comme avant. | true |
| leapClingMinSeconds | Durée la plus courte qu'une prise puisse tirer. | 2.0 |
| leapClingMaxSeconds | Durée la plus longue. Les deux champs sont triés avant le tirage, donc écrire un maximum sous le minimum inverse simplement les bornes au lieu de casser la prise. | 5.0 |
| leapClingDamage | Points de vie retirés par seconde de prise. À 0, le zombie tient sa proie sans lui faire de mal. | 1.0 |
| leapClingReachSq | Distance horizontale au carré en dessous de laquelle l'atterrissage compte comme une touche. | 1.0 |
| leapClingMaxDropBelow | Blocs sous la proie où le zombie peut encore s'accrocher. | 1.0 |
| leapClingCooldownActivations | Activations avant qu'un même zombie puisse reprendre. | 24 |
Les dix-sept champs vivent sous Leap dans
config/oas/lethalbreed.json et se règlent aussi par
les trois interfaces. Éteindre
leapClingEnabled pendant une prise n'y met pas fin sur-le-champ : celle qui court
va au bout de sa durée, aucune autre ne commence.