Flottes, vols et combat
Ce qui vole, ce qui arrive sur vous, combien de temps un trajet prendra, ce qu'une flotte peut porter et ce qu'elle vaut au combat.
Dix-huit fonctions. Quatre interrogent le jeu, quatorze calculent sur ce que le robot a déjà en mémoire. C'est la première chose à savoir, parce qu'un script qui boucle sur cent cibles fait cent requêtes ou aucune selon celle qu'il appelle.
Ce qui coûte une requête, et ce qui n'en coûte pas
| Fonction | Ce qu'elle coûte |
|---|---|
GetFleets, GetAttacks, IsUnderAttack | une requête à chaque appel |
FlightTime | une requête à chaque appel, même sur une cible que vous venez de chiffrer |
| Les quatorze autres | rien : elles lisent la mémoire du robot |
Le cas de FlightTime mérite d'être vu de près. Avant de calculer, elle demande au jeu quels systèmes vides ou inactifs sont sautés sur le trajet, et cette question part à chaque appel, sur tous les univers, y compris ceux qui ne sautent rien. Dans une boucle de cent cibles, c'est cent requêtes.
for cible in cibles {
if Distance(depart, cible) > 5000 {
continue // gratuit, aucune requête
}
// on ne chiffre que les finalistes
secs, fuel = FlightTime(depart, cible, HUNDRED_PERCENT, flotte, TRANSPORT)
if secs > 0 && secs < 3600 {
Print(cible, ShortDur(secs), "deut", fuel)
}
}
Une exception à connaître. Les fonctions de calcul lisent trois choses dans la mémoire du robot : vos recherches, vos bonus de forme de vie et votre classe d'alliance. Si cette mémoire est encore vide, juste après une reconnexion, la première lecture va les chercher, et cela peut coûter jusqu'à trois pages. Les calculs de soute lisent en plus l'écran d'envoi de flotte, une page de plus, quand le robot ne l'a pas vu depuis un quart d'heure. Les suivantes sont gratuites. Un ShipCargo(LARGECARGO) au démarrage réchauffe le tout d'un coup et vous débarrasse de la question.
Zéro veut dire « je n'ai pas pu lire »
Aucune fonction de cette famille ne rend d'erreur. C'est voulu, et il faut savoir pourquoi : anko empaquette les retours multiples dans une tranche, si bien qu'un if IsUnderAttack() sur une fonction qui rendrait (bool, error) serait vrai à chaque tour, ciel vide compris. Voir le premier piège.
Quand la lecture échoue, vous recevez donc la valeur nulle : 0, une liste vide, des emplacements à zéro. Elle ne se distingue pas d'un vrai zéro dans votre script, mais elle laisse une trace : le robot écrit au journal, en niveau Warn, une ligne api de flotte : lecture impossible qui nomme la fonction et la raison. Si un chiffre vous paraît absurde, c'est là qu'il faut regarder.
Deux conséquences pratiques :
FlightTimequi rend0seconde n'annonce pas un trajet instantané : aucun vol réel ne dure zéro seconde. C'est un échec, ou une flotte vide.GetFleetsqui échoue rend des emplacements tous à zéro, pour lesquelsInUse < Totalest faux etIsAllSlotsInUserend vrai. Un script prudent s'abstient donc d'envoyer, ce qui est la bonne réaction quand on ne sait rien.
Ce qui vole, et ce qui arrive
| Fonction | Rend | |
|---|---|---|
GetFleets() | 2 : les vols et les emplacements | Vos flottes en l'air, pas celles qui vous visent. |
GetAttacks() | 1 : une liste | Les flottes hostiles en route vers votre compte. |
IsUnderAttack() | 1 : un booléen | Vrai s'il y en a au moins une. |
flottes, emplacements = GetFleets() // DEUX cibles
Print(len(flottes), "vols,", emplacements.InUse, "/", emplacements.Total)
for f in flottes {
if f.ReturnFlight { continue }
Print(f.Origin, "vers", f.Destination, "dans", f.ArriveIn, "s")
}
Un vol porte Mission, Origin, Destination, Ships, Resources, ReturnFlight, InDeepSpace, StartTime, ArrivalTime, BackTime, ArriveIn et BackIn (des secondes), ID, UnionID, TargetPlanetID, et répond à IsCancellable(), qui est vrai tant que le vol n'est ni un retour, ni perdu dans l'espace profond, ni un tir de missiles.
Sur un vol de retour, ArriveIn vaut -1. Ce n'est pas un échec de lecture, c'est la convention du jeu : le compte à rebours qui reste est dans BackIn. Un script qui trie ses vols par ArriveIn mettra donc tous ses retours en tête.
Les emplacements portent InUse, Total, ExpInUse, ExpTotal, et répondent à IsAllSlotsInUse(mission), qui regarde en plus les emplacements d'expédition quand la mission en est une. GetSlots() les rend seuls, sans la liste des vols (voir Compte, galaxie, joueurs et messages).
Une attaque, vue de près
Un événement d'attaque porte ID, AttackerName, AttackerID, MissionType, Origin, Destination, DestinationName, ArrivalTime, ArriveIn, Missiles, UnionID et Ships.
Ships peut être nul. Le jeu ne publie la composition que si vous la voyez ; sans cela le champ n'est pas rempli du tout. Et quand le jeu affiche « ? » pour un type de vaisseau, la quantité vaut -1, pas zéro : un script qui additionne sans regarder obtient un total plus petit que la réalité.
for a in GetAttacks() {
Print(a.AttackerName, "arrive sur", a.DestinationName, "dans", a.ArriveIn, "s")
if a.Ships == nil { Print(" composition inconnue") }
}
IsUnderAttack fait exactement la même requête que GetAttacks. Les appeler toutes les deux dans la même passe coûte deux fois le prix : préférez GetAttacks et testez la longueur.
Écart avec Ninja. Ces deux fonctions ne passent pas par la lecture d'origine, qui répondait « aucune attaque » en permanence sur les serveurs en version 13. Elles lisent la liste d'événements du jeu, la seule qui dise la vérité sur ces univers.
Distance et durée de vol
| Fonction | Rend | |
|---|---|---|
Distance(origine, cible) | 1 : un entier | La distance dans l'unité du jeu. Aucune requête. |
FlightTime(origine, cible, vitesse, flotte, mission, [stationnement]) | 2 : les secondes et le deutérium | Une requête à chaque appel. |
Les deux coordonnées s'écrivent au choix : une chaîne "1:2:3" (avec le préfixe P:, M: ou D: si vous voulez autre chose qu'une planète), une coordonnée construite avec NewCoordinate, ou directement un céleste, dont la coordonnée est prise toute seule.
depart = GetCachedCelestials()[0]
Print(Distance(depart, "1:2:4")) // le céleste suffit, pas besoin de .GetCoordinate()
Distance tient compte de votre univers : nombre de galaxies et de systèmes, et bouclage en anneau s'il est activé. Deux points au même emplacement rendent 5. Attention, le type de céleste n'entre pas dans le calcul : une planète et sa lune sont à distance 5 l'une de l'autre, comme deux fois la même coordonnée.
En revanche Distance ne retire pas les systèmes vides ou inactifs que le jeu saute : lui seul sait lesquels le sont. C'est toute la différence de prix avec FlightTime, qui va le lui demander.
Les trois arguments qui trompent
La vitesse n'est pas un pourcentage. Utilisez les constantes : HUNDRED_PERCENT vaut 10, FIFTY_PERCENT vaut 5, TEN_PERCENT vaut 1, FIVE_PERCENT vaut 0.5, par pas de cinq pour cent. Au-delà de 10, la valeur est ramenée à HUNDRED_PERCENT sans avertissement : écrire 50 en pensant 50 % fait partir la flotte à pleine vitesse, et FlightTime chiffre ce vol-là. Une vitesse nulle ou négative est refusée : FlightTime rend 0, 0 et le journal dit pourquoi.
La mission n'est pas décorative : elle choisit le facteur de vitesse de flotte de l'univers. ATTACK, GROUPEDATTACK, DESTROY, MISSILEATTACK, RECYCLEDEBRISFIELD et SPY prennent la vitesse de guerre, toutes les autres la vitesse pacifique. Sur un univers où les deux diffèrent, se tromper de mission fausse la durée.
Le stationnement, sixième argument, est en heures, et n'existe pas dans la signature de Ninja. Il est accepté en plus parce que le jeu facture le maintien en deutérium : sans lui, le carburant annoncé pour une expédition est celui du seul trajet, donc trop bas. Omis, il vaut zéro. Ne le donnez que pour EXPEDITION et PARKINTHATALLY : le supplément est ajouté dès qu'il est non nul, quelle que soit la mission, et sur une autre mission le carburant sortirait gonflé d'un coût que le jeu ne prend pas.
flotte = NewShipsInfos()
flotte.Set(LARGECARGO, 20)
secs, fuel = FlightTime(depart, "1:2:4", HUNDRED_PERCENT, flotte, EXPEDITION, 2)
Print(ShortDur(secs), fuel) // ShortDur lit des secondes
Une flotte vide rend 0, 0, exactement comme un échec, et coûte tout de même la requête : la question au jeu part avant que le calcul remarque qu'il n'y a rien à faire voler.
Trouver où envoyer, pour rentrer après telle heure
C'est la question de tout sauvetage de flotte écrit à la main : où envoyer mes vaisseaux pour qu'ils reviennent après mon réveil ? Quatre fonctions y répondent, chacune sur une sorte de destination.
| Fonction | Rend | |
|---|---|---|
FindDebrisFieldWithMinimumTravelTime(origine, flotte, minimum) | 4 | Un champ de ruines. |
FindEmptyPlanetWithMinimumTravelTime(origine, flotte, minimum) | 4 | Une place vide. |
FindInactivePlanetWithMinimumTravelTime(origine, flotte, minimum, avecLune) | 4 | Un joueur inactif. |
FindAbandonedPlanetWithMinimumTravelTime(origine, flotte, minimum, avecLune) | 4 | Une planète détruite. |
Les quatre valeurs sont, dans l'ordre : la destination, la durée du vol en secondes, le carburant, et une erreur. Comptez-les : c'est le piège numéro deux de la page d'accueil.
time = import("time")
p = GetCachedPlanets()[0]
flotte, err = p.GetShips()
dest, secs, deut, err = FindDebrisFieldWithMinimumTravelTime(p.GetCoordinate(),
flotte, 7 * time.Hour)
if err != nil {
LogWarn("aucune destination :", err)
} else {
Printf("%v, %.1f h de vol, %d deutérium", dest, secs / 3600, deut)
}
La durée minimale s'écrit des deux façons
FindDebrisFieldWithMinimumTravelTime(o, f, 7 * time.Hour) // ✓
FindDebrisFieldWithMinimumTravelTime(o, f, 25200) // ✓ le même
L'outil de référence passe une durée Go ; Kepler compte partout ailleurs en secondes. Les deux sont acceptées.
Anko ne garde pas le type d'une durée multipliée, et c'est bon à savoir bien au-delà d'ici :
Printf("%T", time.Hour) // time.Duration
Printf("%T", 1 * time.Hour) // int64 ← la multiplication perd le type
L'entier qui en sort porte des nanosecondes. Ces fonctions le reconnaissent : au-delà d'un an, un entier nu ne peut plus être des secondes — personne ne demande à sa flotte de rester dehors un an — tandis qu'en nanosecondes la plus courte durée qu'on écrive, une minute, vaut déjà soixante milliards. Rien de sensé ne tombe entre les deux.
Vous pouvez donc recopier un script de l'outil de référence sans y toucher. Si vous écrivez la durée vous-même, écrivez-la en secondes : c'est l'unité de toute cette référence.
La durée rendue est toujours en secondes, elle aussi, comme FlightTime.
Tout se calcule à la vitesse la plus lente
Dix pour cent, c'est-à-dire le vol le plus long possible. C'est la définition de l'outil de référence, et c'est la seule qui ait un sens ici : on cherche à rester dehors longtemps.
Une fois la destination trouvée, vous partez à la vitesse que vous voulez — mais un vol lancé à 100 % rentrera dix fois plus tôt que le chiffre rendu.
Aucune requête au jeu, et ce que cela implique
Ces quatre fonctions ne lisent que les relevés de galaxie déjà en base, ceux que le Scanner a rapportés. Vous pouvez donc les appeler en boucle sans rien coûter au compte.
En contrepartie : ce que le Scanner n'a jamais vu n'existe pas pour elles.
FindDebrisFieldWithMinimumTravelTime : aucune position de cette sorte
dans les relevés
Cette erreur-là ne dit pas que la galaxie est vide : elle dit que votre Scanner n'est pas encore passé. L'autre erreur possible est plus fine :
FindInactivePlanetWithMinimumTravelTime : rien d'assez loin dans les
relevés, le plus lointain est en deçà du minimum
Là, il y a bien des cibles, mais aucune assez loin pour tenir votre nuit. Élargissez la zone du Scanner, ou baissez le minimum.
Ce qui est rendu au juste
La destination porte le bon genre : DEBRIS pour un champ de ruines, PLANET pour les trois autres. Passez-la telle quelle à NewFleet, le genre suit.
Parmi les destinations qui atteignent le minimum, c'est la plus proche qui est rendue : c'est la moins chère en carburant, et celle qui rentre le plus tôt après l'heure demandée.
La durée est celle du calcul de la bibliothèque, systèmes vides non sautés. Elle surestime donc légèrement le vol réel — ce qui est le bon sens de l'erreur pour un sauvetage : on rentre plus tôt que prévu, jamais plus tard.
Ce que « abandonnée » veut dire
Une planète détruite par une étoile de la mort. Le jeu la laisse dans la galaxie, sans joueur. Le robot la reconnaît à cela : un nom de planète, et personne dessus.
avecLune ne garde que les positions qui portent une lune, pour les deux fonctions qui l'acceptent.
Ce qu'une flotte porte et à quelle vitesse
| Fonction | Rend | |
|---|---|---|
Speed(flotte) | 1 : un entier | La vitesse du vaisseau le plus lent, celle qui dicte le vol. |
Cargo(flotte) | 1 : un entier | Le fret total, ressources confondues. |
Les deux acceptent la flotte par valeur ou par pointeur, ce qui n'est pas le cas chez Ninja : ses exemples écrivent Speed(*s1), et Speed(s1) marche tout aussi bien ici. Un pointeur nul est lu comme une flotte vide, sans plantage.
f = NewShipsInfos()
f.Set(LARGECARGO, 10)
Print(Speed(f), Cargo(f))
Speed rend zéro sur une flotte sans vaisseau volant, un parc de satellites solaires ou de foreuses par exemple. La bibliothèque, elle, rendrait le plus grand entier possible, chiffre qui traverserait un calcul de vol entier sans se faire remarquer : la garde est de nous.
Vos technologies, votre classe de personnage, vos bonus de forme de vie et les réglages de l'univers sont déjà dans ces chiffres. La classe d'alliance aussi, mais elle ne pèse que dans un seul cas : seule la classe Commerçant compte, et seulement sur le petit et le grand transporteur, à qui elle donne dix pour cent de vitesse en plus. Guerrier et Chercheur ne changent rien à ces nombres, ni ici ni dans FlightTime.
Un vaisseau à l'unité
Ce que un exemplaire vaut sur votre compte, tout bonus compris.
| Fonction | Rend | |
|---|---|---|
ShipSpeed(id) | 1 : un entier | Sa vitesse. |
ShipFuel(id) | 1 : un entier | Sa consommation de référence, celle qui entre dans le calcul du carburant. Jamais moins de 1. |
ShipCargo(id) | 1 : un entier | Sa soute. |
Print(ShipCargo(LARGECARGO)) // 25000 de base, davantage avec l'hyperespace
La soute est celle que le jeu applique. L'écran d'envoi de flotte publie la soute de chaque vaisseau, bonus compris. ShipCargo, Cargo, la méthode Cargo() d'une flotte, CalcCargo, CalcFastCargo et CalcPreferredCargo rendent la plus petite des deux, celle du calcul et celle du jeu : un calcul qui verrait plus grand que le jeu donnerait trop peu de transporteurs, et le jeu refuserait l'envoi (soute insuffisante, code 140028). L'envoi, lui, suit la soute de l'écran qu'il vient de charger. Le robot relit cet écran à chaque envoi ; juste après un démarrage, ou quand sa dernière lecture a plus d'un quart d'heure, le premier de ces calculs le charge une fois, sauf en mode manuel, où il se fait sans lui.
Trois paliers du jeu sont bien pris en compte, et ils surprennent quand on compare à la valeur de base :
- Petit transporteur, propulsion à impulsion 5. Il change de moteur : sa vitesse de base passe de 5 000 à 10 000, et sa consommation double.
- Recycleur. À impulsion 17 sa consommation double, à hyperespace 15 elle triple. Le second palier remplace le premier, il ne s'y ajoute pas.
- Général. Il divise la consommation par deux, sur tous ses vaisseaux.
Une sonde d'espionnage rend une soute de zéro si votre univers n'autorise pas les sondes à piller. Ce n'est pas une erreur de lecture, c'est le réglage du serveur.
Un identifiant qui n'est pas un vaisseau (une défense, un bâtiment) rend 0 et une ligne au journal.
Composer un transport
| Fonction | Rend | |
|---|---|---|
CalcCargo(total) | 2 : des grands transporteurs, OU des petits | Deux solutions séparées, pas les deux moitiés d'une flotte. |
CalcFastCargo(gtDispo, ptDispo, total) | 3 : des grands, des petits, et le fret obtenu | La composition que votre parc autorise. |
res = NewResources(10000, 20000, 30000)
gt, pt = CalcCargo(res.Total())
Print("soit " + gt + " grands transporteurs, soit " + pt + " petits")
gt, pt, fret = CalcFastCargo(10, 8, res.Total())
Print(gt, "grands et", pt, "petits, pour", fret, "de fret")
Le compte est arrondi vers le haut : une unité de ressource en trop demande un vaisseau de plus.
« Rapide » veut dire le moins de grands transporteurs possible. La règle est « le plus petit nombre de grands tel que les petits disponibles finissent la charge », et elle est appliquée telle quelle, sans jamais consulter la vitesse réelle de votre parc.
Elle suppose que le petit transporteur vole plus vite que le grand, ce qui n'est vrai qu'à partir de la propulsion à impulsion 5. Avant ce palier c'est l'inverse : le grand transporteur va exactement une fois et demie plus vite que le petit, quel que soit votre niveau de combustion. Sur un compte qui n'a pas encore l'impulsion 5, la composition rendue par CalcFastCargo est donc la plus lente des deux, et c'est CalcCargo qu'il vous faut.
Quand votre parc ne suffit pas, CalcFastCargo ne refuse pas : elle rend le parc entier, et le troisième chiffre, plus petit que le total demandé, est ce qui vous le dit. Testez-le, sinon vous laissez du butin sur place sans le savoir.
gt, pt, fret = CalcFastCargo(gtDispo, ptDispo, total)
if fret < total { Print("il restera", total - fret, "sur place") }
Un total nul ou négatif rend 0, 0, 0, et les disponibilités négatives sont lues comme zéro.
Attention aux exemples de Ninja : ceux qui annoncent « 0 et 9 » puis « 1 et 4 » pour 60 000 ressources supposent un petit transporteur de 7 000, c'est à dire un Collecteur avec hyperespace 3. À soute de base la même règle rend « 1 et 7 » dans les deux cas, et c'est juste. Ne prenez pas ces chiffres pour un test.
Puissance de combat
Les trois valeurs d'un affrontement, pour un exemplaire de l'unité, vaisseau comme défense.
| Fonction | Rend | |
|---|---|---|
AttackStrength(id, recherches) | 1 : un entier | Puissance de feu, plus 10 % de la valeur de base par niveau d'armes. |
ShieldPower(id, recherches) | 1 : un entier | Bouclier, plus 10 % par niveau de bouclier. |
StructuralIntegrity(id, recherches) | 1 : un entier | Structure, plus 10 % par niveau de blindage. |
Les recherches sont un argument, et non les vôtres d'office : c'est ainsi qu'on pèse la défense d'une cible à partir d'un rapport d'espionnage. Passez nil pour prendre les vôtres.
Print(AttackStrength(LIGHTFIGHTER, nil)) // avec vos recherches
Print(AttackStrength(LIGHTFIGHTER, recherchesCible)) // avec les siennes
Avec des recherches fournies, ces trois fonctions ne touchent ni au jeu ni à votre compte : elles marchent même sans session ouverte.
DBGetResearches n'existe pas chez nous, et les exemples de Ninja l'appellent. Passez nil, ou GetResearch(), qui rend vos recherches en une valeur et coûte une requête.
Un identifiant qui n'est ni un vaisseau ni une défense rend 0.
Prix et durées de construction
Ces trois-là ne concernent pas les flottes, mais elles sont du même bois : de l'arithmétique locale, sans une requête.
| Fonction | Rend | |
|---|---|---|
GetPrice(id, nbr) | 1 : des ressources | nbr est un niveau pour un bâtiment ou une recherche, un nombre d'exemplaires pour un vaisseau ou une défense. |
ConstructionTime(id, nbr, installations) | 1 : une durée | Durée de construction, installations à l'appui. |
TechnologyConstructionTime(id, niveau, laboratoire) | 1 : une durée | Durée d'une recherche, niveau de laboratoire donné en clair. |
p = GetPrice(METALMINE, 20)
Print(p.Metal, p.Crystal, p.Deuterium, "total", p.Total())
GetPrice rend le prix de CE niveau, pas le cumul depuis le niveau zéro. Total() additionne métal, cristal et deutérium ; Value() les pondère 1, 2 et 3.
Ni l'un ni l'autre ne compte l'énergie, que le prix porte pourtant dans p.Energy. Le cas extrême est la technologie Graviton, qui ne coûte que de l'énergie : GetPrice(GRAVITONTECHNOLOGY, 1).Total() rend 0. Un script qui trie des chantiers par leur coût la placera en tête, gratuite.
ConstructionTime prend les installations du céleste : usine de robots et nanites pour un bâtiment, chantier spatial et nanites pour un vaisseau ou une défense, laboratoire pour une recherche. Il n'existe pas de constructeur d'installations : il faut les lire, avec GetFacilities(céleste), qui rend deux valeurs. Une valeur qui n'est pas des installations rend une durée nulle et une ligne au journal.
inst, err = GetFacilities(celeste)
if err == nil {
Print(ShortDur(ConstructionTime(LARGECARGO, 20, inst)))
Print(ShortDur(TechnologyConstructionTime(ESPIONAGETECHNOLOGY, 10, inst.ResearchLab)))
}
CalculateIrnLabLevel, que Ninja emploie pour trouver le laboratoire à passer, n'existe pas chez nous : prenez inst.ResearchLab, quitte à le corriger vous-même si vous exploitez le réseau intergalactique.
Écart avec Ninja. Une recherche passée à ConstructionTime y donne une durée différente de celle de TechnologyConstructionTime, parce que la bibliothèque lui applique la seule vitesse d'économie de l'univers, là où une recherche se divise aussi par la vitesse de recherche. Chez nous les deux fonctions prennent le même chemin et rendent le même chiffre. Si vous comparez à un script venu d'ailleurs, c'est là que l'écart se trouve.
Et pour envoyer, vraiment
Rien dans cette famille n'envoie de flotte : tout y sert à préparer et à vérifier. L'envoi passe par une seule fonction.
| Fonction | Rend | |
|---|---|---|
NewFleet() | 1 : un constructeur | À enchaîner. nil sans session ouverte. |
Elle rend un constructeur que l'on enchaîne, et dont SendNow() rend deux valeurs, le vol et une erreur.
f = NewFleet()
f.SetOrigin(depart)
f.SetDestination("1:2:4")
f.SetMission(TRANSPORT)
f.SetSpeed(HUNDRED_PERCENT)
f.AddShips(LARGECARGO, gt)
f.SetAllResources()
vol, err = f.SendNow() // DEUX cibles
if err != nil { LogError("envoi :", err) }
Sans session ouverte, NewFleet() rend nil et le script s'arrête sur la première méthode appelée, plutôt que de vous laisser croire qu'il a envoyé quelque chose. Le journal dit pourquoi.
Rappeler automatiquement
f.SetRecallIn(secondes) avant SendNow() rappelle la flotte ce nombre de secondes après son départ. Le rappel passe par les mêmes contrôles que CancelFleet : il attend pendant que vous jouez à la main, ne reconnecte jamais un compte qui dort, et prend son tour avec les workers. Il a lieu même après la fin de votre script : un script peut envoyer, puis s'arrêter. Si la flotte se pose avant que le rappel ait pu partir, le journal dit qu'il a été abandonné.
secs, carburant = f.FlightTime()
f.SetRecallIn(secs * 45 / 100) // demi-tour à 45 % de l'aller
vol, err = f.SendNow()
La phalange
| Fonction | Rend | |
|---|---|---|
Phalanx(lune, cible) | 2 : les vols et une erreur | Relève ce qui vole autour d'une position. |
Elle part d'une de vos lunes portant un capteur, et vise une position dans sa portée. Chaque appel coûte du deutérium et le jeu impose un délai entre deux relevés : ce n'est pas une lecture gratuite, c'est un geste.
lune = GetCachedCelestial("M:1:2:3")
vols, err = Phalanx(lune.GetID(), "1:2:8")
if err != nil {
LogError("phalange :", err)
} else {
for v in vols {
Print(v.ID, v.Mission, v.Origin, v.Destination, v.ArrivalTime)
}
}
Un refus du jeu arrive comme une erreur, jamais comme une liste vide. C'est le piège de cette fonction, et Kepler l'écarte pour vous : pas de planète à cette position, pas assez de deutérium, pas de capteur sur la lune — tout cela ressemble, dans la réponse brute, à un relevé qui n'a rien vu. Vous recevez le motif du jeu lui-même, qui nomme précisément ce qui manque.
Une liste vide sans erreur veut donc dire ce qu'elle dit : la position est calme.
Chaque vol porte son propre identifiant. L'extracteur de la bibliothèque les perd, et sans eux tous les vols du relevé porteraient le même numéro : un script qui les range par identifiant n'en garderait qu'un. Kepler les remet depuis le relevé brut.
Deux refus qu'il vaut mieux connaître d'avance
Mesurés en jeu le 05/09/2026, parce qu'aucun des deux ne se devine.
Un scan trop rapproché du précédent rend « Paramètre non valide ». Le jeu impose un délai de quelques secondes entre deux relevés, et son refus ne le dit pas : il accuse vos arguments, qui n'y sont pour rien. Trois scans lancés à la suite dans une boucle donnent une réussite puis deux de ces messages. Espacez d'environ quatre secondes — SleepSec(4) entre deux appels suffit.
Chaque scan coûte du deutérium sur la lune, et quand elle n'en a plus le jeu le dit franchement : « Pas assez de deutérium disponible ! ». Une boucle qui balaye un système entier vide donc la lune, puis échoue sur ce message.
lune = GetCachedCelestial("M:3:193:8")
sys, err = GalaxyInfos(3, 193)
for i = 1; i <= 15; i++ {
p = sys.Position(i)
if p == nil { continue }
vols, err = Phalanx(lune.GetID(), p.Coordinate)
if err != nil { LogWarn(i, ":", err) } else { Print(i, len(vols), "vols") }
SleepSec(4)
}
Rappeler une flotte
| Fonction | Rend | |
|---|---|---|
CancelFleet(flotte) | 1 : une erreur | Fait faire demi-tour à un vol en cours. |
Le rappel n'est pas un retour immédiat. La flotte fait demi-tour et met à revenir exactement le temps qu'elle a déjà parcouru : rappeler à mi-chemin coûte donc autant que d'aller au bout. La fonction rend la main dès que le jeu a accepté l'ordre, pas quand la flotte est rentrée. Un retour à nil ne dit pas « elle est chez vous », il dit « elle revient ».
Elle accepte trois écritures, parce que trois sont naturelles : l'identifiant tel que GetFleets le rend, la fiche de vol entière, ou l'entier nu que vous auriez rangé dans le stockage du script.
vols, emplacements = GetFleets()
for vol in vols {
err = CancelFleet(vol.ID)
if err != nil { LogError("rappel :", err) }
}
Une flotte déjà rentrée, ou qui revient déjà, fait rendre une erreur du jeu. Ce n'est pas une panne du script : c'est un ordre qui n'avait plus d'objet.
La porte de saut
| Fonction | Rend | |
|---|---|---|
JumpGate(origine, destination, vaisseaux) | 1 : une erreur | Saute des vaisseaux d'une lune à l'autre. |
JumpGateDestinations(origine) | 3 : les lunes, la recharge, une erreur | Ce qui est joignable, et dans combien de temps. |
JumpGate2(origine, destination, vaisseaux) | 3 : la réussite, la recharge, une erreur | Le même saut, avec le délai en nombre. |
Les deux veulent des lunes. Une planète passée à leur place fait rendre une erreur qui la nomme, plutôt que le refus muet du jeu.
La porte se recharge après chaque saut. JumpGate sur une porte qui n'est pas prête ne rend pas un false silencieux : elle rend une erreur qui dit le temps restant, seule chose qu'un script puisse en faire.
Pour attendre la porte, prenez JumpGate2. Elle fait exactement le même geste, mais rend la recharge en secondes, comme un nombre : un script qui veut dormir jusqu'à la prochaine fenêtre n'a pas à relire une phrase pour y retrouver un chiffre. La recharge n'y est pas une erreur — c'est la réponse.
parti, recharge, err = JumpGate2(lune.GetID(), destinations[0], vaisseaux)
if err != nil { LogError("saut :", err); return }
if !parti {
LogInfo("porte occupée, on repasse dans", ShortDur(recharge * 1000000000))
SleepSec(recharge)
}
lune = GetCachedCelestial("M:1:2:3")
destinations, recharge, err = JumpGateDestinations(lune.GetID())
if err != nil { LogError("porte :", err) }
Print("joignables :", len(destinations), "recharge :", recharge)
vaisseaux = NewShipsInfos()
vaisseaux.Set(LARGECARGO, 100)
err = JumpGate(lune.GetID(), destinations[0], vaisseaux)
if err != nil { LogError("saut :", err) }
Le saut ne consomme pas d'emplacement de flotte et ne coûte pas de deutérium : c'est ce qui le rend intéressant pour déplacer une défense mobile ou vider une lune menacée. En revanche il ne transporte que des vaisseaux, jamais de ressources.
L'expédition de forme de vie, position par position
| Fonction | Rend | |
|---|---|---|
SendDiscoveryFleet(origine, destination) | 1 : une erreur | Une expédition sur UNE position. |
À ne pas confondre avec le worker Discovery, qui couvre un système entier d'un clic et gère lui-même la réserve d'emplacements. Celle-ci vise une position et une seule, et ne compte rien : c'est à votre script de vérifier qu'il reste de la place. GetSlots et GetFleetSlotsReserved sont là pour cela.
slots = GetSlots()
if slots.Total - slots.InUse <= GetFleetSlotsReserved() {
return // on laisse la place au Defender
}
err = SendDiscoveryFleet("1:64:8", "1:64:12")
if err != nil { LogError("découverte :", err) }
La destination se donne telle qu'on l'a sous la main. Une chaîne "1:64:12", une coordonnée, l'un de vos célestes ou son identifiant, ou tout élément de liste qui porte un champ Coordinate : une position de GalaxyInfos ou de GetPlanetInfo, une planète de PlayerDataByID. Un résumé de rapport de GetEspionageReportMessages passe aussi, pour son Target. Le cas le plus courant est un élément de la liste que rend CoordinatesAvailableForDiscoveryFleet : c'est déjà une coordonnée, passez-le tel quel. el.Coordinate n'existe pas sur lui et arrête le script.
origine = GetCachedPlanets()[0]
coords, err = CoordinatesAvailableForDiscoveryFleet(origine, 1, 64)
if err != nil { LogError("galaxie :", err); return }
for el in coords {
Print(SendDiscoveryFleet(origine, el)) // <nil> : parti
SleepRandSec(20, 60)
}
Une place vide de GalaxyInfos vaut nil. sys.Position(i) rend nil sur une place vide, justement là où une découverte part souvent, et la fonction refuse nil en le disant. Passez NewCoordinate(g, s, i, PLANET_TYPE) à la place, ou mieux, parcourez CoordinatesAvailableForDiscoveryFleet, qui ne liste que les positions ouvertes, places vides comprises.
Une flotte n'est pas une destination : elle porte une origine ET une destination, et c'est à vous de dire laquelle (f.Destination). Un rapport de combat non plus, pour la même raison. Ce qui ne porte pas de coordonnée est refusé par une phrase qui nomme le type reçu, sans qu'aucune requête ne parte ; une position hors de 1 à 15 aussi.
Ce que Print en montre. La fonction rend une erreur et rien d'autre : nil quand le jeu a accepté l'envoi, que Print écrit <nil>, comme chez l'outil de référence. Un refus s'imprime avec la phrase du jeu, précédée de découverte impossible :.
Deux requêtes par appel. La fonction relève d'abord le système depuis l'origine, puis envoie : le jeu ne délivre le jeton de l'envoi qu'avec ce relevé, et ne l'accepte qu'une fois. Une position que le relevé dit au repos est refusée sans envoi. Dans une boucle, espacez les appels comme ci-dessus : quinze départs à la seconde près ne ressemblent à aucun joueur.
Ce qu'elle coûte vraiment. Aucun de vos vaisseaux ne part, mais l'envoi prend un emplacement de flotte et des ressources sur le céleste d'origine. Il demande en outre la technologie des Émissaires, et le jeu impose un délai par position : une position visitée récemment refuse la suivante.
Le refus vient du jeu et le nomme. Un script qui balaie un système entier doit donc s'attendre à ce que certaines positions le refusent, et ce n'est pas une panne — c'est le délai qui court.
Le vol à l'envers : partir de l'heure d'arrivée
| Fonction | Rend | |
|---|---|---|
GetDepartureTime(arrivée, origine, destination, flotte, vitesse, mission) | 1 : une heure | Quand décoller pour arriver à l'heure dite. |
LowestSpeed(arrivée, origine, destination, flotte) | 4 : l'allure, le carburant, l'heure de départ, une erreur | L'allure la moins chère qui parte encore. |
Pourquoi ce sens-là compte. Une sauvegarde de flotte se pense par sa fin : « je veux que tout soit rentré à huit heures ». Le sens direct — je pars maintenant, j'arrive quand — oblige le script à tâtonner, et à tâtonner avec des requêtes, puisque chaque FlightTime en coûte une.
flotte = NewShipsInfos()
flotte.Set(LARGECARGO, 10)
depart = GetDepartureTime("08:00:00", "1:64:8", "1:64:12", flotte, TEN_PERCENT, TRANSPORT)
<-ExecAtCh(depart, func() {
LogInfo("c'est l'heure de partir")
})
LowestSpeed cherche la plus LENTE qui parte encore, et c'est bien la question posée. Plus on va lentement, plus il faut décoller tôt : à 5 %, le départ est le plus souvent déjà passé. L'allure utile est donc la moins chère en deutérium parmi celles qu'on peut encore prendre. Si même 100 % part trop tard, l'heure voulue est hors d'atteinte, et l'erreur le dit — plutôt que de rendre une allure impossible à tenir.
allure, deut, depart, err = LowestSpeed("08:00:00", "1:64:8", "1:64:12", flotte)
if err != nil { LogError(err); return }
Printf("partir à %s à %d%%, %d deutérium", depart.Format("15:04"), allure * 10, deut)
Un décollage exactement maintenant ne compte pas. Le temps de composer la flotte et de cliquer, la seconde est passée : l'allure au-dessus est retenue, et vous arrivez à l'heure.
Ce que ça coûte. GetDepartureTime fait une requête, celle du calcul de vol. LowestSpeed en fait six : une pour la borne haute, puis cinq au plus pour la recherche. La durée de vol décroît quand l'allure monte, si bien que la réponse se trouve par dichotomie sur les allures du compte (vingt pour un Général, les dix dizaines pour les autres classes) plutôt que par un balayage — mesuré, cinq essais suffisent dans le pire cas. Six requêtes, ce n'est pas à mettre dans une boucle sur cent cibles : c'est fait pour être appelé une fois, au moment de planifier.
L'heure d'arrivée s'écrit des deux façons : "08:00:00" désigne la prochaine fois qu'il sera cette heure-là, comme ParseNextDatetimeAt, ou une date calculée avec time.
Confier la surveillance au robot
| Fonction | Rend | |
|---|---|---|
StartPhalanxSession(lune, cible, cadence) | 2 : un identifiant et une erreur | Règle la surveillance d'une position et l'allume. |
StopPhalanxSession(identifiant) | 1 : une erreur | L'éteint. |
À ne pas confondre avec Phalanx, qui scanne une fois et rend ce qu'elle voit. Celles-ci confient la surveillance au robot : il scanne à la cadence donnée, tant qu'on ne l'arrête pas, et les vols relevés partent aux alertes comme le reste.
lune = GetCachedCelestial("M:1:2:3")
id, err = StartPhalanxSession(lune.GetID(), "1:2:8", 300) // toutes les 5 min
if err != nil { LogError("phalange :", err); return }
SleepMin(60)
StopPhalanxSession(id)
La cadence se compte en secondes, et chaque scan coûte cinq mille deutérium. Une cadence trop courte vide une lune en une nuit sans rien apprendre de plus : le jeu ne rafraîchit pas plus vite.
Écart avec Ninja. Là-bas, plusieurs surveillances tournent de front et chacune a son identifiant. Kepler n'en tient qu'une — une lune, une cible, comme la page de l'interface — et rend donc toujours 1. StopPhalanxSession arrête celle qui tourne, quel que soit l'identifiant qu'on lui passe. Un script recopié tourne, mais il ne surveillera qu'une position à la fois.
Le réglage survit à l'arrêt : rallumer depuis la page n'oblige pas à resaisir la lune et la cible.
Le fret, en privilégiant un type
| Fonction | Rend | |
|---|---|---|
CalcPreferredCargo(préféré, sondes, éclaireurs, grands, petits, total, arrondi) | 5 : sondes, éclaireurs, grands, petits, le fret | Compose un transport en servant d'abord le type voulu. |
Quatre soutes, là où CalcFastCargo n'en connaît que deux. La sonde d'espionnage — quand le serveur autorise le pillage par sondes — et l'éclaireur s'ajoutent au grand et au petit transporteur. Sur un univers qui les autorise, une sonde porte pour une fraction du deutérium d'un transporteur ; les ignorer coûte cher.
sondes, eclaireurs, gt, pt, fret = CalcPreferredCargo(LARGECARGO, 0, 0, 50, 20, 5000000, true)
Printf("%d GT + %d PT emportent %d", gt, pt, fret)
Le type préféré est servi en premier, jusqu'à épuisement de ce qui est disponible ; les autres complètent, du plus grand au plus petit — on veut le moins de vaisseaux possible, pas le plus. Un type préféré dont le parc est vide ne provoque pas d'erreur : le reste se charge.
L'arrondi décide du dernier vaisseau. À vrai, un reste d'une unité de ressource demande un vaisseau de plus et la charge part entière ; à faux, ce reste est laissé sur place. Un fret rendu plus petit que la charge demandée veut donc dire l'un ou l'autre : le parc n'a pas suffi, ou l'arrondi était à faux.
L'attaque groupée
| Fonction | Rend | |
|---|---|---|
CreateUnion(vol, joueurs) | 2 : le numéro de l'union et une erreur | Ouvre une attaque groupée autour d'un vol. |
L'ordre compte, et c'est le piège. Le vol doit être parti avant qu'on puisse inviter qui que ce soit : on envoie d'abord sa propre flotte en attaque groupée, on relit son identifiant, et c'est seulement là qu'on ouvre l'union. Le numéro rendu est celui que les invités passent à leur propre envoi.
Les noms sont ceux des joueurs, tels que le jeu les écrit. Un nom mal frappé n'est pas refusé ici : c'est le jeu qui le dira, et lui seul sait qui existe.