691 lines
16 KiB
ActionScript
691 lines
16 KiB
ActionScript
import empire_ai.weasel.WeaselAI;
|
|
|
|
import util.design_export;
|
|
import util.random_designs;
|
|
|
|
interface RaceDesigns {
|
|
bool preCompose(DesignTarget@ target);
|
|
bool postCompose(DesignTarget@ target);
|
|
bool design(DesignTarget@ target, int size, const Design@& output);
|
|
};
|
|
|
|
enum DesignPurpose {
|
|
DP_Scout,
|
|
DP_Combat,
|
|
DP_Defense,
|
|
DP_Support,
|
|
DP_Gate,
|
|
DP_Slipstream,
|
|
DP_Mothership,
|
|
DP_Miner,
|
|
|
|
DP_COUNT,
|
|
DP_Unknown,
|
|
};
|
|
|
|
tidy final class DesignTarget {
|
|
int id = -1;
|
|
const Design@ active;
|
|
string customName;
|
|
|
|
array<const Design@> potential;
|
|
array<double> scores;
|
|
|
|
uint purpose;
|
|
double targetBuildCost = 0;
|
|
double targetMaintenance = 0;
|
|
double targetLaborCost = 0;
|
|
|
|
double dps = 0.0;
|
|
double hp = 0.0;
|
|
double supplyDrain = 0.0;
|
|
|
|
double targetSize = 0;
|
|
bool findSize = false;
|
|
|
|
Designer@ designer;
|
|
|
|
DesignTarget() {
|
|
}
|
|
|
|
DesignTarget(uint type, double targetSize) {
|
|
this.purpose = type;
|
|
this.targetSize = targetSize;
|
|
}
|
|
|
|
uint get_designType() {
|
|
switch(purpose) {
|
|
case DP_Scout: return DT_Flagship;
|
|
case DP_Combat: return DT_Flagship;
|
|
case DP_Defense: return DT_Station;
|
|
case DP_Support: return DT_Support;
|
|
case DP_Gate: return DT_Station;
|
|
case DP_Slipstream: return DT_Flagship;
|
|
case DP_Mothership: return DT_Flagship;
|
|
}
|
|
return DT_Flagship;
|
|
}
|
|
|
|
void save(Designs& designs, SaveFile& file) {
|
|
if(active !is null) {
|
|
file.write1();
|
|
file << active;
|
|
file << dps;
|
|
file << supplyDrain;
|
|
file << hp;
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
|
|
file << purpose;
|
|
file << targetBuildCost;
|
|
file << targetMaintenance;
|
|
file << targetLaborCost;
|
|
file << targetSize;
|
|
file << findSize;
|
|
file << customName;
|
|
}
|
|
|
|
void load(Designs& designs, SaveFile& file) {
|
|
if(file.readBit()) {
|
|
file >> active;
|
|
file >> dps;
|
|
file >> supplyDrain;
|
|
file >> hp;
|
|
}
|
|
|
|
file >> purpose;
|
|
file >> targetBuildCost;
|
|
file >> targetMaintenance;
|
|
file >> targetLaborCost;
|
|
file >> targetSize;
|
|
file >> findSize;
|
|
file >> customName;
|
|
}
|
|
|
|
void prepare(AI& ai) {
|
|
@designer = Designer(designType, targetSize, ai.empire, compose=false);
|
|
designer.randomHull = true;
|
|
|
|
switch(purpose) {
|
|
case DP_Scout:
|
|
designer.composeScout();
|
|
break;
|
|
case DP_Combat:
|
|
designer.composeFlagship();
|
|
break;
|
|
case DP_Defense:
|
|
designer.composeStation();
|
|
break;
|
|
case DP_Support:
|
|
designer.composeSupport();
|
|
break;
|
|
case DP_Gate:
|
|
designer.composeGate();
|
|
break;
|
|
case DP_Slipstream:
|
|
designer.composeSlipstream();
|
|
break;
|
|
case DP_Mothership:
|
|
designer.composeMothership();
|
|
break;
|
|
}
|
|
}
|
|
|
|
double weight(double value, double goal) {
|
|
if(value < goal)
|
|
return sqr(value / goal);
|
|
else if(value > goal * 1.5)
|
|
return goal / value;
|
|
return 1.0;
|
|
}
|
|
|
|
double costWeight(double value, double goal) {
|
|
if(findSize) {
|
|
if(value < goal)
|
|
return 1.0;
|
|
else
|
|
return 0.000001;
|
|
}
|
|
else {
|
|
if(value < goal)
|
|
return goal / value;
|
|
else
|
|
return pow(0.2, ((value / goal) - 1.0) * 10.0);
|
|
}
|
|
}
|
|
|
|
double evaluate(AI& ai, const Design& dsg) {
|
|
double w = 1.0;
|
|
|
|
//Try to stick as close to our target as we can
|
|
if(targetBuildCost != 0)
|
|
w *= costWeight(dsg.total(HV_BuildCost), targetBuildCost);
|
|
if(targetLaborCost != 0)
|
|
w *= costWeight(dsg.total(HV_LaborCost), targetLaborCost);
|
|
if(targetMaintenance != 0)
|
|
w *= costWeight(dsg.total(HV_MaintainCost), targetMaintenance);
|
|
|
|
double predictHP = 0.0;
|
|
double predictDPS = 0.0;
|
|
double predictDrain = 0.0;
|
|
|
|
//Value support capacity where appropriate
|
|
if(purpose == DP_Combat) {
|
|
double supCap = dsg.total(SV_SupportCapacity);
|
|
double avgHP = 0, avgDPS = 0, avgDrain = 0.0;
|
|
cast<Designs>(ai.designs).getSupportAverages(avgHP, avgDPS, avgDrain);
|
|
|
|
predictHP += supCap * avgHP;
|
|
predictDPS += supCap * avgDPS;
|
|
predictDrain += supCap * avgDrain;
|
|
}
|
|
|
|
//Value combat strength where appropriate
|
|
if(purpose != DP_Scout && purpose != DP_Slipstream && purpose != DP_Mothership) {
|
|
predictDPS += dsg.total(SV_DPS);
|
|
predictHP += dsg.totalHP + dsg.total(SV_ShieldCapacity);
|
|
predictDrain += dsg.total(SV_SupplyDrain);
|
|
|
|
if(purpose != DP_Support) {
|
|
w *= (predictHP * predictDPS) * 0.001;
|
|
|
|
double supplyStores = dsg.total(SV_SupplyCapacity);
|
|
double actionTime = supplyStores / predictDrain;
|
|
w *= weight(actionTime, ai.behavior.fleetAimSupplyDuration);
|
|
}
|
|
}
|
|
|
|
//Value acceleration on a target
|
|
if(purpose != DP_Defense && purpose != DP_Gate) {
|
|
double targetAccel = 2.0;
|
|
if(purpose == DP_Support)
|
|
targetAccel *= 1.5;
|
|
else if(purpose == DP_Scout)
|
|
targetAccel *= 3.0;
|
|
|
|
w *= weight(dsg.total(SV_Thrust) / max(dsg.total(HV_Mass), 0.01), targetAccel);
|
|
}
|
|
|
|
//Penalties for having important systems easy to shoot down
|
|
uint holes = 0;
|
|
for(uint i = 0, cnt = dsg.subsystemCount; i < cnt; ++i) {
|
|
auto@ sys = dsg.subsystem(i);
|
|
if(!sys.type.hasTag(ST_Important))
|
|
continue;
|
|
//TODO: We should be able to penalize for exposed supply storage
|
|
if(sys.type.hasTag(ST_NoCore))
|
|
continue;
|
|
|
|
vec2u core = sys.core;
|
|
for(uint d = 0; d < 6; ++d) {
|
|
if(!traceContainsArmor(dsg, core, d))
|
|
holes += 1;
|
|
}
|
|
}
|
|
|
|
if(holes != 0)
|
|
w /= pow(0.9, double(holes));
|
|
|
|
//TODO: Check FTL
|
|
|
|
return w;
|
|
}
|
|
|
|
bool traceContainsArmor(const Design@ dsg, const vec2u& startPos, uint direction) {
|
|
vec2u pos = startPos;
|
|
while(dsg.hull.active.valid(pos)) {
|
|
if(!dsg.hull.active.advance(pos, HexGridAdjacency(direction)))
|
|
break;
|
|
|
|
auto@ sys = dsg.subsystem(pos.x, pos.y);
|
|
if(sys is null)
|
|
continue;
|
|
if(sys.type.hasTag(ST_IsArmor))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool contains(const Design& dsg) {
|
|
if(active is null)
|
|
return false;
|
|
if(dsg.mostUpdated() is active.mostUpdated())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
const Design@ design(AI& ai, Designs& designs) {
|
|
int trySize = targetSize;
|
|
if(findSize) {
|
|
trySize = randomd(0.75, 1.25) * targetSize;
|
|
trySize = 5 * round(double(designer.size) / 5.0);
|
|
}
|
|
if(designs.race !is null) {
|
|
const Design@ fromRace;
|
|
if(designs.race.design(this, trySize, fromRace))
|
|
return fromRace;
|
|
}
|
|
if(designer !is null) {
|
|
designer.size = trySize;
|
|
return designer.design(1);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void choose(AI& ai, const Design@ dsg, bool randomizeName=true) {
|
|
set(dsg);
|
|
@designer = null;
|
|
findSize = false;
|
|
|
|
string baseName = dsg.name;
|
|
if(customName.length != 0) {
|
|
baseName = customName;
|
|
}
|
|
else if(randomizeName) {
|
|
if(dsg.hasTag(ST_IsSupport))
|
|
baseName = autoSupportNames[randomi(0,autoSupportNames.length-1)];
|
|
else
|
|
baseName = autoFlagNames[randomi(0,autoFlagNames.length-1)];
|
|
}
|
|
|
|
string name = baseName;
|
|
uint try = 0;
|
|
while(ai.empire.getDesign(name) !is null) {
|
|
name = baseName + " ";
|
|
appendRoman(++try, name);
|
|
}
|
|
if(name != dsg.name)
|
|
dsg.rename(name);
|
|
|
|
//Set design settings/support behavior
|
|
if(purpose == DP_Support) {
|
|
if(dsg.total(SV_SupportSupplyCapacity) > 0.01) {
|
|
DesignSettings settings;
|
|
settings.behavior = SG_Brawler;
|
|
dsg.setSettings(settings);
|
|
}
|
|
else if(dsg.totalHP > 50 * dsg.size) {
|
|
DesignSettings settings;
|
|
settings.behavior = SG_Shield;
|
|
dsg.setSettings(settings);
|
|
}
|
|
else {
|
|
DesignSettings settings;
|
|
settings.behavior = SG_Cannon;
|
|
dsg.setSettings(settings);
|
|
}
|
|
}
|
|
|
|
|
|
ai.empire.addDesign(ai.empire.getDesignClass("AI", true), dsg);
|
|
|
|
if(cast<Designs>(ai.designs).log)
|
|
ai.print("Chose design for purpose "+uint(purpose)+" at size "+dsg.size);
|
|
}
|
|
|
|
void step(AI& ai, Designs& designs) {
|
|
if(active is null) {
|
|
if(designer is null) {
|
|
if(designs.race is null || !designs.race.preCompose(this))
|
|
prepare(ai);
|
|
if(designs.race !is null && designs.race.postCompose(this))
|
|
return;
|
|
}
|
|
if(potential.length >= ai.behavior.designEvaluateCount) {
|
|
//Find the best design out of all our potentials
|
|
const Design@ best;
|
|
double bestScore = 0.0;
|
|
|
|
for(uint i = 0, cnt = potential.length; i < cnt; ++i) {
|
|
double w = scores[i];
|
|
if(w > bestScore) {
|
|
bestScore = w;
|
|
@best = potential[i];
|
|
}
|
|
}
|
|
potential.length = 0;
|
|
scores.length = 0;
|
|
|
|
if(best !is null)
|
|
choose(ai, best);
|
|
}
|
|
else if(designer !is null && active is null) {
|
|
//Add a new design onto the list to be evaluated
|
|
const Design@ dsg = design(ai, designs);
|
|
if(dsg !is null && !dsg.hasFatalErrors()) {
|
|
potential.insertLast(dsg);
|
|
scores.insertLast(evaluate(ai, dsg));
|
|
|
|
/*if(designs.log)*/
|
|
/* ai.print("Designed for purpose "+uint(purpose)+" at size "+dsg.size+", weight "+evaluate(ai, dsg));*/
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
set(active.mostUpdated());
|
|
}
|
|
}
|
|
|
|
void set(const Design@ dsg) {
|
|
if(active is dsg)
|
|
return;
|
|
|
|
@active = dsg;
|
|
targetBuildCost = dsg.total(HV_BuildCost);
|
|
targetMaintenance = dsg.total(HV_MaintainCost);
|
|
targetLaborCost = dsg.total(HV_LaborCost);
|
|
targetSize = dsg.size;
|
|
|
|
dps = dsg.total(SV_DPS);
|
|
hp = dsg.totalHP + dsg.total(SV_ShieldCapacity);
|
|
supplyDrain = dsg.total(SV_SupplyDrain);
|
|
}
|
|
};
|
|
|
|
const Design@ scaleDesign(const Design@ orig, int newSize) {
|
|
DesignDescriptor desc;
|
|
resizeDesign(orig, newSize, desc);
|
|
|
|
return makeDesign(desc);
|
|
}
|
|
|
|
final class Designs : AIComponent {
|
|
RaceDesigns@ race;
|
|
|
|
int nextTargetId = 0;
|
|
array<DesignTarget@> designing;
|
|
array<DesignTarget@> completed;
|
|
array<DesignTarget@> automatic;
|
|
|
|
void create() {
|
|
@race = cast<RaceDesigns>(ai.race);
|
|
}
|
|
|
|
void start() {
|
|
//Design some basic support sizes
|
|
design(DP_Support, 1);
|
|
design(DP_Support, 2);
|
|
design(DP_Support, 4);
|
|
design(DP_Support, 8);
|
|
design(DP_Support, 16);
|
|
}
|
|
|
|
void save(SaveFile& file) {
|
|
file << nextTargetId;
|
|
|
|
uint cnt = designing.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
saveDesign(file, designing[i]);
|
|
designing[i].save(this, file);
|
|
}
|
|
|
|
cnt = automatic.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
saveDesign(file, automatic[i]);
|
|
if(!isDesigning(automatic[i])) {
|
|
file.write1();
|
|
automatic[i].save(this, file);
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
}
|
|
|
|
cnt = completed.length;
|
|
file << cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
saveDesign(file, completed[i]);
|
|
if(!isDesigning(completed[i])) {
|
|
file.write1();
|
|
completed[i].save(this, file);
|
|
}
|
|
else {
|
|
file.write0();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isDesigning(DesignTarget@ targ) {
|
|
for(uint i = 0, cnt = designing.length; i < cnt; ++i) {
|
|
if(designing[i] is targ)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void load(SaveFile& file) {
|
|
file >> nextTargetId;
|
|
|
|
uint cnt = 0;
|
|
file >> cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
auto@ targ = loadDesign(file);
|
|
targ.load(this, file);
|
|
designing.insertLast(targ);
|
|
}
|
|
|
|
file >> cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
auto@ targ = loadDesign(file);
|
|
if(file.readBit())
|
|
targ.load(this, file);
|
|
automatic.insertLast(targ);
|
|
}
|
|
|
|
file >> cnt;
|
|
for(uint i = 0; i < cnt; ++i) {
|
|
auto@ targ = loadDesign(file);
|
|
if(file.readBit())
|
|
targ.load(this, file);
|
|
completed.insertLast(targ);
|
|
}
|
|
}
|
|
|
|
array<DesignTarget@> loadIds;
|
|
DesignTarget@ loadDesign(int id) {
|
|
if(id == -1)
|
|
return null;
|
|
for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) {
|
|
if(loadIds[i].id == id)
|
|
return loadIds[i];
|
|
}
|
|
DesignTarget data;
|
|
data.id = id;
|
|
loadIds.insertLast(data);
|
|
return data;
|
|
}
|
|
DesignTarget@ loadDesign(SaveFile& file) {
|
|
int id = -1;
|
|
file >> id;
|
|
if(id == -1)
|
|
return null;
|
|
else
|
|
return loadDesign(id);
|
|
}
|
|
void saveDesign(SaveFile& file, DesignTarget@ data) {
|
|
int id = -1;
|
|
if(data !is null)
|
|
id = data.id;
|
|
file << id;
|
|
}
|
|
void postLoad(AI& ai) {
|
|
loadIds.length = 0;
|
|
}
|
|
|
|
const Design@ get_currentSupport() {
|
|
for(int i = automatic.length - 1; i >= 0; --i) {
|
|
if(automatic[i].purpose == DP_Support && automatic[i].active !is null)
|
|
return automatic[i].active;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void getSupportAverages(double& hp, double& dps, double& supDrain) {
|
|
hp = 0;
|
|
dps = 0;
|
|
supDrain = 0;
|
|
uint count = 0;
|
|
for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
|
|
auto@ targ = automatic[i];
|
|
if(targ.purpose != DP_Support)
|
|
continue;
|
|
if(targ.active is null)
|
|
continue;
|
|
|
|
hp += targ.hp / double(targ.targetSize);
|
|
dps += targ.dps / double(targ.targetSize);
|
|
supDrain += targ.supplyDrain / double(targ.targetSize);
|
|
count += 1;
|
|
}
|
|
|
|
if(count == 0) {
|
|
hp = 40.0;
|
|
dps = 0.30;
|
|
supDrain = 1.0;
|
|
}
|
|
else {
|
|
hp /= double(count);
|
|
dps /= double(count);
|
|
supDrain /= double(count);
|
|
}
|
|
}
|
|
|
|
DesignPurpose classify(Object@ obj) {
|
|
if(obj is null || !obj.isShip)
|
|
return DP_Combat;
|
|
Ship@ ship = cast<Ship>(obj);
|
|
return classify(ship.blueprint.design);
|
|
}
|
|
|
|
DesignPurpose classify(const Design@ dsg, DesignPurpose defaultPurpose = DP_Combat) {
|
|
if(dsg is null)
|
|
return defaultPurpose;
|
|
|
|
for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
|
|
if(automatic[i].contains(dsg))
|
|
return DesignPurpose(automatic[i].purpose);
|
|
}
|
|
|
|
for(uint i = 0, cnt = completed.length; i < cnt; ++i) {
|
|
if(completed[i].contains(dsg))
|
|
return DesignPurpose(completed[i].purpose);
|
|
}
|
|
|
|
if(dsg.hasTag(ST_Mothership))
|
|
return DP_Mothership;
|
|
if(dsg.hasTag(ST_Gate))
|
|
return DP_Gate;
|
|
if(dsg.hasTag(ST_Slipstream))
|
|
return DP_Slipstream;
|
|
if(dsg.hasTag(ST_Support))
|
|
return DP_Support;
|
|
if(dsg.hasTag(ST_Station))
|
|
return DP_Defense;
|
|
|
|
double dps = dsg.total(SV_DPS);
|
|
if(dsg.total(SV_MiningRate) > 0)
|
|
return DP_Miner;
|
|
if(dsg.size == 16.0 && dsg.total(SV_DPS) < 2.0)
|
|
return DP_Scout;
|
|
if(dps > 0.1 * dsg.size || dsg.total(SV_SupportCapacity) > 0)
|
|
return DP_Combat;
|
|
return defaultPurpose;
|
|
}
|
|
|
|
DesignTarget@ design(uint purpose, int size, int targetCost = 0, int targetMaint = 0, double targetLabor = 0, bool findSize = false) {
|
|
for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
|
|
auto@ target = automatic[i];
|
|
if(target.purpose != purpose)
|
|
continue;
|
|
if(target.targetSize != size)
|
|
continue;
|
|
if(targetCost != 0 && target.targetBuildCost > targetCost)
|
|
continue;
|
|
if(targetMaint != 0 && target.targetMaintenance > targetMaint)
|
|
continue;
|
|
if(targetLabor != 0 && target.targetLaborCost > targetLabor)
|
|
continue;
|
|
if(target.findSize != findSize)
|
|
continue;
|
|
return target;
|
|
}
|
|
|
|
DesignTarget targ(purpose, size);
|
|
targ.findSize = findSize;
|
|
targ.targetBuildCost = targetCost;
|
|
targ.targetMaintenance = targetMaint;
|
|
targ.targetLaborCost = targetLabor;
|
|
|
|
automatic.insertLast(targ);
|
|
return design(targ);
|
|
}
|
|
|
|
DesignTarget@ design(DesignTarget@ target) {
|
|
target.id = nextTargetId++;
|
|
designing.insertLast(target);
|
|
return target;
|
|
}
|
|
|
|
DesignTarget@ get(const Design@ dsg) {
|
|
for(uint i = 0, cnt = automatic.length; i < cnt; ++i) {
|
|
if(automatic[i].contains(dsg))
|
|
return automatic[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
DesignTarget@ scale(const Design@ dsg, int newSize) {
|
|
if(dsg.newer !is null) {
|
|
auto@ newTarg = get(dsg.newest());
|
|
if(newTarg.targetSize == newSize)
|
|
return newTarg;
|
|
@dsg = dsg.newest();
|
|
}
|
|
|
|
DesignTarget@ previous = get(dsg);
|
|
|
|
uint purpose = DP_Combat;
|
|
if(previous !is null)
|
|
purpose = previous.purpose;
|
|
else
|
|
purpose = classify(dsg);
|
|
|
|
DesignTarget target(purpose, newSize);
|
|
target.id = nextTargetId++;
|
|
@target.active = scaleDesign(dsg, newSize);
|
|
|
|
ai.empire.changeDesign(dsg, target.active, ai.empire.getDesignClass(dsg.cls.name, true));
|
|
|
|
if(previous !is null)
|
|
automatic.remove(previous);
|
|
automatic.insertLast(target);
|
|
|
|
return target;
|
|
}
|
|
|
|
uint chkInd = 0;
|
|
void tick(double time) {
|
|
if(designing.length != 0) {
|
|
//chkInd = (chkInd+1) % designing.length;
|
|
// Getting 1 design first is better than getting all of them later
|
|
chkInd = 0;
|
|
auto@ target = designing[chkInd];
|
|
target.step(ai, this);
|
|
|
|
if(target.active !is null) {
|
|
designing.removeAt(chkInd);
|
|
if(automatic.find(target) == -1)
|
|
completed.insertLast(target);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
AIComponent@ createDesigns() {
|
|
return Designs();
|
|
}
|