Starsector API
Loading...
Searching...
No Matches
MoteControlScript.java
Go to the documentation of this file.
1package com.fs.starfarer.api.impl.combat;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.Iterator;
6import java.util.List;
7import java.util.Map;
8
9import java.awt.Color;
10
11import org.lwjgl.util.vector.Vector2f;
12
13import com.fs.starfarer.api.Global;
14import com.fs.starfarer.api.combat.BaseEveryFrameCombatPlugin;
15import com.fs.starfarer.api.combat.CollisionClass;
16import com.fs.starfarer.api.combat.CombatEngineAPI;
17import com.fs.starfarer.api.combat.CombatEngineLayers;
18import com.fs.starfarer.api.combat.CombatEntityAPI;
19import com.fs.starfarer.api.combat.DamageType;
20import com.fs.starfarer.api.combat.EmpArcEntityAPI;
21import com.fs.starfarer.api.combat.EmpArcEntityAPI.EmpArcParams;
22import com.fs.starfarer.api.combat.EveryFrameCombatPlugin;
23import com.fs.starfarer.api.combat.MissileAPI;
24import com.fs.starfarer.api.combat.MutableShipStatsAPI;
25import com.fs.starfarer.api.combat.ShipAPI;
26import com.fs.starfarer.api.combat.ShipSystemAPI;
27import com.fs.starfarer.api.combat.ShipSystemAPI.SystemState;
28import com.fs.starfarer.api.combat.WeaponAPI.WeaponSize;
29import com.fs.starfarer.api.impl.campaign.ids.HullMods;
30import com.fs.starfarer.api.input.InputEventAPI;
31import com.fs.starfarer.api.loading.WeaponSlotAPI;
32import com.fs.starfarer.api.util.IntervalUtil;
33import com.fs.starfarer.api.util.Misc;
34import com.fs.starfarer.api.util.WeightedRandomPicker;
35
37
38 protected static float MAX_ATTRACTOR_RANGE = 3000f;
39 public static float MAX_DIST_FROM_SOURCE_TO_ENGAGE_AS_PD = 2000f;
40 public static float MAX_DIST_FROM_ATTRACTOR_TO_ENGAGE_AS_PD = 1000f;
41
42 public static int MAX_MOTES = 30;
43 public static int MAX_MOTES_HF = 50;
44
45 public static float ANTI_FIGHTER_DAMAGE = 200;
46 public static float ANTI_FIGHTER_DAMAGE_HF = 1000;
47
48 public static float ATTRACTOR_DURATION_LOCK = 20f;
49 public static float ATTRACTOR_DURATION = 10f;
50
51 //public static Color JITTER_COLOR = new Color(100,155,255,175);
52
53
54// static {
55// // .wpn
56// Color muzzleFlashColor = new Color(100,165,255,25);
57//
58// // .proj
59// Color hitGlowColor = new Color(100,100,255,255);
60// Color engineGlowColor = new Color(100,165,255,255);
61// Color contrailColor = new Color(100,165,255,25);
62//
63// //MoteControlScript
64// Color jitterColor = new Color(100,165,255,175);
65// Color empArcColor = new Color(100,165,255,255);
66//
67// // on hit effect
68// Color onHitEmpColor = new Color(100,165,255,255);
69// }
70
71 public static class MoteData {
72 public Color jitterColor;
73 public Color empColor;
74
75 public int maxMotes;
76
77 public float antiFighterDamage;
78 public String impactSound;
79 public String loopSound;
80 }
81
82 public static Map<String, MoteData> MOTE_DATA = new HashMap<String, MoteData>();
83
84 public static String MOTELAUNCHER = "motelauncher";
85 public static String MOTELAUNCHER_HF = "motelauncher_hf";
86
87 static {
88 MoteData normal = new MoteData();
89 normal.jitterColor = new Color(100,165,255,175);
90 normal.empColor = new Color(100,165,255,255);
91 normal.maxMotes = MAX_MOTES;
92 normal.antiFighterDamage = ANTI_FIGHTER_DAMAGE;
93 normal.impactSound = "mote_attractor_impact_normal";
94 normal.loopSound = "mote_attractor_loop";
95
96 MOTE_DATA.put(MOTELAUNCHER, normal);
97
98 MoteData hf = new MoteData();
99 hf.jitterColor = new Color(255,100,255,175);
100 hf.empColor = new Color(255,100,255,255);
101 hf.maxMotes = MAX_MOTES_HF;
102 hf.antiFighterDamage = ANTI_FIGHTER_DAMAGE_HF;
103 hf.impactSound = "mote_attractor_impact_damage";
104 hf.loopSound = "mote_attractor_loop_dark";
105
106 MOTE_DATA.put(MOTELAUNCHER_HF, hf);
107 }
108
109 public static boolean isHighFrequency(ShipAPI ship) {
110 //if (true) return true;
111 return ship != null && ship.getVariant().hasHullMod(HullMods.HIGH_FREQUENCY_ATTRACTOR);
112 }
113
114 public static String getWeaponId(ShipAPI ship) {
115 if (isHighFrequency(ship)) return MOTELAUNCHER_HF;
116 return MOTELAUNCHER;
117 }
118
119 public static float getAntiFighterDamage(ShipAPI ship) {
120 return MOTE_DATA.get(getWeaponId(ship)).antiFighterDamage;
121 }
122 public static String getImpactSoundId(ShipAPI ship) {
123 return MOTE_DATA.get(getWeaponId(ship)).impactSound;
124 }
125 public static Color getJitterColor(ShipAPI ship) {
126 return MOTE_DATA.get(getWeaponId(ship)).jitterColor;
127 }
128 public static Color getEMPColor(ShipAPI ship) {
129 return MOTE_DATA.get(getWeaponId(ship)).empColor;
130 }
131
132 public static int getMaxMotes(ShipAPI ship) {
133 return MOTE_DATA.get(getWeaponId(ship)).maxMotes;
134 }
135
136 public static String getLoopSound(ShipAPI ship) {
137 return MOTE_DATA.get(getWeaponId(ship)).loopSound;
138 }
139
140
141 public static class SharedMoteAIData {
142 public float elapsed = 0f;
143 public List<MissileAPI> motes = new ArrayList<MissileAPI>();
144
145 public float attractorRemaining = 0f;
146 public Vector2f attractorTarget = null;
147 public ShipAPI attractorLock = null;
148 }
149
150 public static SharedMoteAIData getSharedData(ShipAPI source) {
151 String key = source + "_mote_AI_shared";
152 SharedMoteAIData data = (SharedMoteAIData)Global.getCombatEngine().getCustomData().get(key);
153 if (data == null) {
154 data = new SharedMoteAIData();
155 Global.getCombatEngine().getCustomData().put(key, data);
156 }
157 return data;
158 }
159
160
161
162 protected IntervalUtil launchInterval = new IntervalUtil(0.75f, 1.25f);
165 protected WeaponSlotAPI attractor = null;
166
167 //protected int empCount = 0;
168 protected boolean findNewTargetOnUse = true;
169
170 protected void findSlots(ShipAPI ship) {
171 if (attractor != null) return;
172 for (WeaponSlotAPI slot : ship.getHullSpec().getAllWeaponSlotsCopy()) {
173 if (slot.isSystemSlot()) {
174 if (slot.getSlotSize() == WeaponSize.SMALL) {
175 launchSlots.add(slot);
176 }
177 if (slot.getSlotSize() == WeaponSize.MEDIUM) {
178 attractor = slot;
179 }
180 }
181 }
182 }
183
184 public void apply(MutableShipStatsAPI stats, String id, State state, float effectLevel) {
185 ShipAPI ship = null;
186 if (stats.getEntity() instanceof ShipAPI) {
187 ship = (ShipAPI) stats.getEntity();
188 } else {
189 return;
190 }
191
193
194 //Global.getCombatEngine().setPaused(true);
195
196 SharedMoteAIData data = getSharedData(ship);
197 data.elapsed += amount;
198
199 if (data.attractorRemaining > 0) {
200 data.attractorRemaining -= amount;
201 if (data.attractorRemaining <= 0 ||
202 (data.attractorLock != null && !data.attractorLock.isAlive()) ||
203 data.motes.isEmpty()) {
204 data.attractorTarget = null;
205 data.attractorLock = null;
206 data.attractorRemaining = 0;
207 }
208 }
209 if (effectLevel <= 0) {
210 findNewTargetOnUse = true;
211 }
212
214
216 if (attractorParticleInterval.intervalElapsed()) {
218 }
219
220 launchInterval.advance(amount * 5f);
221 if (launchInterval.intervalElapsed()) {
222 Iterator<MissileAPI> iter = data.motes.iterator();
223 while (iter.hasNext()) {
224 if (!engine.isMissileAlive(iter.next())) {
225 iter.remove();
226 }
227 }
228
229 if (ship.isHulk()) {
230 for (MissileAPI mote : data.motes) {
231 mote.flameOut();
232 }
233 data.motes.clear();
234 return;
235 }
236
237 int maxMotes = getMaxMotes(ship);
238 if (data.motes.size() < maxMotes && data.attractorLock == null &&// false &&
240 findSlots(ship);
241
242 WeaponSlotAPI slot = launchSlots.pick();
243
244 Vector2f loc = slot.computePosition(ship);
245 float dir = slot.computeMidArcAngle(ship);
246 float arc = slot.getArc();
247 dir += arc * (float) Math.random() - arc /2f;
248
249 String weaponId = getWeaponId(ship);
250 MissileAPI mote = (MissileAPI) engine.spawnProjectile(ship, null,
251 weaponId,
252 loc, dir, null);
253 mote.setWeaponSpec(weaponId);
254 mote.setMissileAI(new MoteAIScript(mote));
256 // if they could flame out/be affected by emp, that'd be bad since they don't expire for a
257 // very long time so they'd be stuck disabled permanently, for practical purposes
258 // thus: total emp resistance (which can't target them anyway, but if it did.)
259 mote.setEmpResistance(10000);
260 data.motes.add(mote);
261
262 engine.spawnMuzzleFlashOrSmoke(ship, slot, mote.getWeaponSpec(), 0, dir);
263
264 Global.getSoundPlayer().playSound("mote_attractor_launch_mote", 1f, 0.25f, loc, new Vector2f());
265 }
266 }
267
268 float maxMotes = getMaxMotes(ship);
269 float fraction = data.motes.size() / (Math.max(1f, maxMotes));
270 float volume = fraction * 3f;
271 if (volume > 1f) volume = 1f;
272 if (data.motes.size() > 3) {
273 Vector2f com = new Vector2f();
274 for (MissileAPI mote : data.motes) {
275 Vector2f.add(com, mote.getLocation(), com);
276 }
277 com.scale(1f / data.motes.size());
278 //Global.getSoundPlayer().playLoop("mote_attractor_loop", ship, 1f, volume, com, new Vector2f());
279 Global.getSoundPlayer().playLoop(getLoopSound(ship), ship, 1f, volume, com, new Vector2f());
280 }
281
282
283 if (effectLevel > 0 && findNewTargetOnUse) {
285 findNewTargetOnUse = false;
286 }
287
288 if (effectLevel == 1) {
289 // possible if system is reused immediately w/ no time to cool down, I think
290 if (data.attractorTarget == null) {
292 }
293 findSlots(ship);
294
295 Vector2f slotLoc = attractor.computePosition(ship);
296
297 CombatEntityAPI asteroid = engine.spawnAsteroid(0, data.attractorTarget.x, data.attractorTarget.y, 0, 0);
299 CombatEntityAPI target = asteroid;
300 if (data.attractorLock != null) {
301 target = data.attractorLock;
302 }
303
304 EmpArcParams params = new EmpArcParams();
305// params.segmentLengthMult = 4f;
306// params.zigZagReductionFactor = 0.25f;
307// params.fadeOutDist = 200f;
308// params.minFadeOutMult = 2f;
309
310 params.segmentLengthMult = 8f;
311 params.zigZagReductionFactor = 0.15f;
312
313 params.brightSpotFullFraction = 0.5f;
314 params.brightSpotFadeFraction = 0.5f;
315 //params.nonBrrightSpotMinBrightness = 0.25f;
316
317 float dist = Misc.getDistance(slotLoc, target.getLocation());
318 params.flickerRateMult = 0.6f - dist / 3000f;
319 if (params.flickerRateMult < 0.3f) {
320 params.flickerRateMult = 0.3f;
321 }
322
323
324// params.fadeOutDist = 500f;
325// params.minFadeOutMult = 2f;
326// params.flickerRateMult = 0.7f;
327 //params.movementDurMax = 0.1f;
328// params.movementDurMin = 0.25f;
329// params.movementDurMax = 0.25f;
330
331 float emp = 0;
332 float dam = 0;
333 EmpArcEntityAPI arc = (EmpArcEntityAPI)engine.spawnEmpArc(ship, slotLoc, ship, target,
335 dam,
336 emp, // emp
337 100000f, // max range
338 "mote_attractor_targeted_ship",
339 40f, // thickness
340 //new Color(100,165,255,255),
342 new Color(255,255,255,255),
343 params
344 );
345 if (data.attractorLock != null) {
346 arc.setTargetToShipCenter(slotLoc, data.attractorLock);
347 }
348 arc.setCoreWidthOverride(30f);
349
350 //arc.setFadedOutAtStart(true);
351 //arc.setRenderGlowAtStart(false);
352 arc.setSingleFlickerMode(true);
353
354 if (data.attractorLock == null) {
355 Global.getSoundPlayer().playSound("mote_attractor_targeted_empty_space", 1f, 1f, data.attractorTarget, new Vector2f());
356 }
357
358// Vector2f targetLoc = new Vector2f(target.getLocation());
359// int glows = 100;
360// float maxRadius = 500f;
361// float minRadius = 300f;
362//
363// if (data.attractorLock != null) {
364// maxRadius += data.attractorLock.getCollisionRadius();
365// minRadius += data.attractorLock.getCollisionRadius();
366// }
367//
368// float minDur = 0.5f;
369// float maxDur = 0.75f;
370// float minSize = 10f;
371// float maxSize = 30f;
372// Color color = MoteControlScript.getEMPColor(ship);
373// for (int i = 0; i < glows; i++) {
374// float radius = minRadius + (float) Math.random() * (maxRadius - minRadius);
375// Vector2f loc = Misc.getPointAtRadius(targetLoc, radius);
376// Vector2f dir = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(loc, targetLoc));
377// float dist = Misc.getDistance(loc, targetLoc);
378//
379// float dur = minDur + (float) Math.random() * (maxDur - minDur);
380// float speed = dist / dur;
381// dir.scale(speed);
382//
383// float size = minSize + (float) Math.random() * (maxSize - minSize);
384//
385// engine.addHitParticle(loc, dir, size, 1f, 0.3f, dur, color);
386// engine.addHitParticle(loc, dir, size * 0.33f, 1f, 0.3f, dur, Color.white);
387// }
388
389
390 engine.removeEntity(asteroid);
391 }
392 }
393
394 protected void spawnAttractorParticles(ShipAPI ship) {
395 if (true) return; // just not liking this much
396 SharedMoteAIData data = getSharedData(ship);
397
398 if (data.attractorTarget == null) return;
399
401
402 Vector2f targetLoc = data.attractorTarget;
403
404 int glows = 2;
405 float maxRadius = 300f;
406 float minRadius = 200f;
407
408 if (data.attractorLock != null) {
409 maxRadius += data.attractorLock.getCollisionRadius();
410 minRadius += data.attractorLock.getCollisionRadius();
411 targetLoc = data.attractorLock.getShieldCenterEvenIfNoShield();
412 }
413
414 float minDur = 0.5f;
415 float maxDur = 0.75f;
416 float minSize = 15f;
417 float maxSize = 30f;
418 Color color = MoteControlScript.getEMPColor(ship);
419 for (int i = 0; i < glows; i++) {
420 float radius = minRadius + (float) Math.random() * (maxRadius - minRadius);
421 Vector2f loc = Misc.getPointAtRadius(targetLoc, radius);
422 Vector2f dir = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(loc, targetLoc));
423 float dist = Misc.getDistance(loc, targetLoc);
424
425 float dur = minDur + (float) Math.random() * (maxDur - minDur);
426 float speed = dist / dur;
427 dir.scale(speed);
428
429 float size = minSize + (float) Math.random() * (maxSize - minSize);
430
431 engine.addHitParticle(loc, dir, size, 1f, 0.3f, dur, color);
432 engine.addHitParticle(loc, dir, size * 0.5f, 1f, 0.3f, dur, Color.white);
433 }
434 }
435
436
437 public void unapply(MutableShipStatsAPI stats, String id) {
438 }
439
440 public StatusData getStatusData(int index, State state, float effectLevel) {
441 return null;
442 }
443
444 public void calculateTargetData(ShipAPI ship) {
445 SharedMoteAIData data = getSharedData(ship);
446 Vector2f targetLoc = getTargetLoc(ship);
447 //System.out.println(getTargetedLocation(ship));
448 data.attractorLock = getLockTarget(ship, targetLoc);
449
450 data.attractorRemaining = ATTRACTOR_DURATION;
451 if (data.attractorLock != null) {
452 targetLoc = new Vector2f(data.attractorLock.getLocation());
453 data.attractorRemaining = ATTRACTOR_DURATION_LOCK;
454 }
455 data.attractorTarget = targetLoc;
456
457 if (data.attractorLock != null) {
458 // need to do this in a script because when the ship is phased, the charge-in time of the system (0.1s)
459 // is not enough for the jitter to come to full effect (which requires 0.1s "normal" time)
463 }
464 }
465
466 @Override
467 public String getInfoText(ShipSystemAPI system, ShipAPI ship) {
468 if (system.isOutOfAmmo()) return null;
469 if (system.getState() != SystemState.IDLE) return null;
470
471 boolean inRange = isMouseInRange(ship);
472 //Vector2f targetLoc = getTargetLoc(ship);
473 //ShipAPI target = getLockTarget(ship, targetLoc);
474
475 if (!inRange) {
476 return "OUT OF RANGE";
477 }
478// if (target != null) {
479// return "ENEMY SHIP";
480// }
481// return "AREA";
482 return null;
483 }
484
485
486 @Override
487 public boolean isUsable(ShipSystemAPI system, ShipAPI ship) {
488 return true;
489 }
490
491 public Vector2f getTargetedLocation(ShipAPI from) {
492 Vector2f loc = from.getSystem().getTargetLoc();
493 if (loc == null) {
494 loc = new Vector2f(from.getMouseTarget());
495 }
496 return loc;
497 }
498
499 public Vector2f getTargetLoc(ShipAPI from) {
500 findSlots(from);
501
502 Vector2f slotLoc = attractor.computePosition(from);
503 Vector2f targetLoc = new Vector2f(getTargetedLocation(from));
504 float dist = Misc.getDistance(slotLoc, targetLoc);
505 if (dist > getRange(from)) {
506 targetLoc = Misc.getUnitVectorAtDegreeAngle(Misc.getAngleInDegrees(slotLoc, targetLoc));
507 targetLoc.scale(getRange(from));
508 Vector2f.add(targetLoc, slotLoc, targetLoc);
509 }
510 return targetLoc;
511 }
512
513 public boolean isMouseInRange(ShipAPI from) {
514 Vector2f targetLoc = new Vector2f(from.getMouseTarget());
515 return isLocationInRange(from, targetLoc);
516 }
517
518 public boolean isLocationInRange(ShipAPI from, Vector2f loc) {
519 findSlots(from);
520
521 Vector2f slotLoc = attractor.computePosition(from);
522 float dist = Misc.getDistance(slotLoc, loc);
523 if (dist > getRange(from)) {
524 return false;
525 }
526 return true;
527 }
528
529
530 public ShipAPI getLockTarget(ShipAPI from, Vector2f loc) {
531 Vector2f slotLoc = attractor.computePosition(from);
532 for (ShipAPI other : Global.getCombatEngine().getShips()) {
533 if (other.isFighter()) continue;
534 if (other.getOwner() == from.getOwner()) continue;
535 if (other.isHulk()) continue;
536 if (!other.isTargetable()) continue;
537
538 float dist = Misc.getDistance(slotLoc, other.getLocation());
539 if (dist > getRange(from)) continue;
540
541 dist = Misc.getDistance(loc, other.getLocation());
542 if (dist < other.getCollisionRadius() + 50f) {
543 return other;
544 }
545 }
546 return null;
547 }
548
549 public static float getRange(ShipAPI ship) {
550 if (ship == null) return MAX_ATTRACTOR_RANGE;
552 }
553
554// public int getMaxMotes() {
555// return MAX_MOTES;
556// }
557
559 final float in,
560 final float out,
561 final Color jitterColor) {
562 return new BaseEveryFrameCombatPlugin() {
563 float elapsed = 0f;
564 @Override
565 public void advance(float amount, List<InputEventAPI> events) {
566 if (Global.getCombatEngine().isPaused()) return;
567
568 elapsed += amount;
569
570
571 float level = 0f;
572 if (elapsed < in) {
573 level = elapsed / in;
574 } else if (elapsed < in + out) {
575 level = 1f - (elapsed - in) / out;
576 level *= level;
577 } else {
579 return;
580 }
581
582
583 if (level > 0) {
584 float jitterLevel = level;
585 float maxRangeBonus = 50f;
586 float jitterRangeBonus = jitterLevel * maxRangeBonus;
587 target.setJitterUnder(this, jitterColor, jitterLevel, 10, 0f, jitterRangeBonus);
588 target.setJitter(this, jitterColor, jitterLevel, 4, 0f, 0 + jitterRangeBonus);
589 }
590 }
591 };
592 }
593}
594
595
596
597
598
599
600
601
static SoundPlayerAPI getSoundPlayer()
Definition Global.java:49
static CombatEngineAPI getCombatEngine()
Definition Global.java:69
float computeEffective(float baseValue)
ShipAPI getLockTarget(ShipAPI from, Vector2f loc)
boolean isUsable(ShipSystemAPI system, ShipAPI ship)
String getInfoText(ShipSystemAPI system, ShipAPI ship)
void apply(MutableShipStatsAPI stats, String id, State state, float effectLevel)
void unapply(MutableShipStatsAPI stats, String id)
WeightedRandomPicker< WeaponSlotAPI > launchSlots
StatusData getStatusData(int index, State state, float effectLevel)
boolean isLocationInRange(ShipAPI from, Vector2f loc)
static SharedMoteAIData getSharedData(ShipAPI source)
EveryFrameCombatPlugin createTargetJitterPlugin(final ShipAPI target, final float in, final float out, final Color jitterColor)
static Vector2f getUnitVectorAtDegreeAngle(float degrees)
Definition Misc.java:1196
static float getDistance(SectorEntityToken from, SectorEntityToken to)
Definition Misc.java:599
static Vector2f getPointAtRadius(Vector2f from, float r)
Definition Misc.java:697
static float getAngleInDegrees(Vector2f v)
Definition Misc.java:1126
void playLoop(String id, Object playingEntity, float pitch, float volume, Vector2f loc, Vector2f vel)
SoundAPI playSound(String id, float pitch, float volume, Vector2f loc, Vector2f vel)
void addHitParticle(Vector2f loc, Vector2f vel, float size, float brightness, float duration, Color color)
void removeEntity(CombatEntityAPI entity)
CombatEntityAPI spawnProjectile(ShipAPI ship, WeaponAPI weapon, String weaponId, Vector2f point, float angle, Vector2f shipVelocity)
void removePlugin(EveryFrameCombatPlugin plugin)
CombatEntityAPI spawnAsteroid(int size, float x, float y, float dx, float dy)
boolean isMissileAlive(MissileAPI missile)
void spawnMuzzleFlashOrSmoke(ShipAPI ship, WeaponSlotAPI slot, WeaponSpecAPI spec, int barrel, float targetAngle)
void addPlugin(EveryFrameCombatPlugin plugin)
EmpArcEntityAPI spawnEmpArc(ShipAPI damageSource, Vector2f point, CombatEntityAPI pointAnchor, CombatEntityAPI empTargetEntity, DamageType damageType, float damAmount, float empDamAmount, float maxRange, String impactSoundId, float thickness, Color fringe, Color core)
void setCollisionClass(CollisionClass collisionClass)
void setTargetToShipCenter(Vector2f sourceSlotPos, ShipAPI ship)
void setCoreWidthOverride(float coreWidthOverride)
EnumSet< CombatEngineLayers > getActiveLayers()
void setWeaponSpec(String weaponId)
void setEmpResistance(int empResistance)
void setMissileAI(MissileAIPlugin ai)
void setJitterUnder(Object source, Color color, float intensity, int copies, float range)
MutableShipStatsAPI getMutableStats()
void setJitter(Object source, Color color, float intensity, int copies, float range)
List< WeaponSlotAPI > getAllWeaponSlotsCopy()
Vector2f computePosition(CombatEntityAPI ship)