Files
Mindustry/core/src/mindustry/entities/def/EntityComps.java
2020-02-05 18:28:19 -05:00

1797 lines
55 KiB
Java

package mindustry.entities.def;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.math.geom.QuadTree.*;
import arc.scene.ui.layout.*;
import arc.struct.Bits;
import arc.struct.Queue;
import arc.struct.*;
import arc.util.*;
import arc.util.ArcAnnotate.*;
import arc.util.pooling.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.ctype.*;
import mindustry.entities.*;
import mindustry.entities.bullet.*;
import mindustry.entities.units.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.input.*;
import mindustry.net.Administration.*;
import mindustry.net.*;
import mindustry.net.Packets.*;
import mindustry.type.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.world.blocks.*;
import mindustry.world.blocks.BuildBlock.*;
import mindustry.world.consumers.*;
import mindustry.world.modules.*;
import java.io.*;
import java.util.*;
import static mindustry.Vars.*;
@SuppressWarnings({"unused", "unchecked"})
public class EntityComps{
@Component
abstract class UnitComp implements Healthc, Velc, Statusc, Teamc, Itemsc, Hitboxc, Rotc, Massc, Unitc, Weaponsc{
private UnitController controller;
private UnitDef type;
@Override
public int itemCapacity(){
return type.itemCapacity;
}
@Override
public float bounds(){
return hitSize() * 2f;
}
@Override
public void controller(UnitController controller){
this.controller = controller;
controller.unit(this);
}
@Override
public UnitController controller(){
return controller;
}
@Override
public void set(UnitDef def, UnitController controller){
type(type);
controller(controller);
}
@Override
public void type(UnitDef type){
this.type = type;
setupWeapons(type);
}
@Override
public UnitDef type(){
return type;
}
@Override
public void update(){
//apply knockback based on spawns
//TODO move elsewhere
if(team() != state.rules.waveTeam){
float relativeSize = state.rules.dropZoneRadius + bounds()/2f + 1f;
for(Tile spawn : spawner.getGroundSpawns()){
if(withinDst(spawn.worldx(), spawn.worldy(), relativeSize)){
vel().add(Tmp.v1.set(this).sub(spawn.worldx(), spawn.worldy()).setLength(0.1f + 1f - dst(spawn) / relativeSize).scl(0.45f * Time.delta()));
}
}
}
Tile tile = tileOn();
Floor floor = floorOn();
if(tile != null){
//unit block update
tile.block().unitOn(tile, this);
//apply damage
if(floor.damageTaken > 0f){
damageContinuous(floor.damageTaken);
}
}
}
@Override
public void drawLight(){
//TODO move
if(type.lightRadius > 0){
renderer.lights.add(getX(), getY(), type.lightRadius, type.lightColor, 0.6f);
}
}
@Override
public void draw(){
//draw power cell - TODO move
Draw.color(Color.black, team().color, healthf() + Mathf.absin(Time.time(), Math.max(healthf() * 5f, 1f), 1f - healthf()));
Draw.rect(type.cellRegion, getX(), getY(), rotation() - 90);
Draw.color();
}
@Override
public void killed(){
float explosiveness = 2f + item().explosiveness * stack().amount;
float flammability = item().flammability * stack().amount;
Damage.dynamicExplosion(getX(), getY(), flammability, explosiveness, 0f, bounds() / 2f, Pal.darkFlame);
//TODO cleanup
//ScorchDecal.create(getX(), getY());
Fx.explosion.at(this);
Effects.shake(2f, 2f, this);
Sounds.bang.at(this);
Events.fire(new UnitDestroyEvent(this));
//TODO implement suicide bomb trigger
//if(explosiveness > 7f && this == player){
// Events.fire(Trigger.suicideBomb);
//}
}
}
@Component
class OwnerComp{
Entityc owner;
}
@Component
abstract class ChildComp implements Posc{
transient float x, y;
private @Nullable Posc parent;
private float offsetX, offsetY;
@Override
public void add(){
if(parent != null){
offsetX = x - parent.getX();
offsetY = y - parent.getY();
}
}
@Override
public void update(){
if(parent != null){
x = parent.getX() + offsetX;
y = parent.getY() + offsetY;
}
}
}
@Component
abstract class BulletComp implements Timedc, Damagec, Hitboxc, Teamc, Posc, Drawc, Shielderc, Ownerc, Velc, Bulletc, Timerc{
private float lifeScl;
Object data;
BulletType type;
float damage;
@Override
public void add(){
type.init(this);
drag(type.drag);
hitSize(type.hitSize);
lifetime(lifeScl * type.lifetime);
}
@Override
public void remove(){
type.despawned(this);
}
@Override
public float getLifetime(){
return type.lifetime;
}
@Override
public float damageMultiplier(){
if(owner() instanceof Unitc){
return ((Unitc)owner()).damageMultiplier();
}
return 1f;
}
@Override
public void absorb(){
//TODO
remove();
}
@Override
public float clipSize(){
return type.drawSize;
}
@Override
public float damage(){
return type.damage * damageMultiplier();
}
@Override
public void collision(Hitboxc other, float x, float y){
if(!type.pierce) remove();
type.hit(this, x, y);
if(other instanceof Unitc){
Unitc unit = (Unitc)other;
unit.vel().add(Tmp.v3.set(other.x(), other.y()).sub(x, y).setLength(type.knockback / unit.mass()));
unit.apply(type.status, type.statusDuration);
}
}
@Override
public void update(){
type.update(this);
if(type.hitTiles){
world.raycastEach(world.toTile(lastX()), world.toTile(lastY()), tileX(), tileY(), (x, y) -> {
Tile tile = world.ltile(x, y);
if(tile == null) return false;
if(tile.entity != null && tile.entity.collide(this) && type.collides(this, tile) && !tile.entity.dead() && (type.collidesTeam || tile.team() != team())){
if(tile.team() != team()){
tile.entity.collision(this);
}
type.hitTile(this, tile);
remove();
return true;
}
return false;
});
}
}
@Override
public void draw(){
type.draw(this);
//TODO refactor
renderer.lights.add(x(), y(), 16f, Pal.powerLight, 0.3f);
}
/** Sets the bullet's rotation in degrees. */
@Override
public void rotation(float angle){
vel().setAngle(angle);
}
/** @return the bullet's rotation. */
@Override
public float rotation(){
float angle = Mathf.atan2(vel().x, vel().y) * Mathf.radiansToDegrees;
if(angle < 0) angle += 360;
return angle;
}
}
@Component
abstract class TimerComp{
Interval timer = new Interval(6);
public boolean timer(int index, float time){
return timer.get(index, time);
}
}
@Component
abstract class DamageComp{
abstract float damage();
}
@Component
abstract class TimedComp implements Entityc, Scaled{
float time, lifetime;
@Override
public void update(){
time = Math.min(time + Time.delta(), lifetime);
if(time >= lifetime){
remove();
}
}
@Override
public float fin(){
return time / lifetime;
}
}
@Component
abstract class HealthComp implements Entityc{
static final float hitDuration = 9f;
float health, maxHealth, hitTime;
boolean dead;
boolean isValid(){
return !dead && isAdded();
}
float healthf(){
return health / maxHealth;
}
@Override
public void update(){
hitTime -= Time.delta() / hitDuration;
}
void killed(){
//implement by other components
}
void kill(){
health = 0;
dead = true;
}
void heal(){
dead = false;
health = maxHealth;
}
boolean damaged(){
return health <= maxHealth - 0.0001f;
}
void damage(float amount){
health -= amount;
if(health <= 0 && !dead){
dead = true;
killed();
}
}
void damage(float amount, boolean withEffect){
float pre = hitTime;
damage(amount);
if(!withEffect){
hitTime = pre;
}
}
void damageContinuous(float amount){
damage(amount * Time.delta(), hitTime <= -20 + hitDuration);
}
void clampHealth(){
health = Mathf.clamp(health, 0, maxHealth);
}
void heal(float amount){
health += amount;
clampHealth();
}
}
@Component
abstract class FlyingComp implements Posc, Velc, Healthc{
transient float x, y;
transient Vec2 vel;
float elevation;
float drownTime;
boolean isGrounded(){
return elevation < 0.001f;
}
@Override
public void update(){
Floor floor = floorOn();
if(isGrounded() && floor.isLiquid && vel.len2() > 0.4f*0.4f && Mathf.chance((vel.len2() * floor.speedMultiplier) * 0.03f * Time.delta())){
floor.walkEffect.at(x, y, 0, floor.color);
}
if(isGrounded() && floor.isLiquid && floor.drownTime > 0){
drownTime += Time.delta() * 1f / floor.drownTime;
drownTime = Mathf.clamp(drownTime);
if(Mathf.chance(Time.delta() * 0.05f)){
floor.drownUpdateEffect.at(x, y, 0f, floor.color);
}
//TODO is the netClient check necessary?
if(drownTime >= 0.999f && !net.client()){
kill();
//TODO drown event!
}
}else{
drownTime = Mathf.lerpDelta(drownTime, 0f, 0.03f);
}
}
}
@Component
abstract class LegsComp implements Posc, Flyingc{
float baseRotation;
}
@Component
abstract class RotComp implements Entityc{
float rotation;
void interpolate(){
Syncc sync = as(Syncc.class);
if(sync.interpolator().values.length > 0){
rotation = sync.interpolator().values[0];
}
}
}
@Component
static abstract class TileComp implements Posc, Teamc, Healthc, Tilec, Timerc{
static final float timeToSleep = 60f * 1;
static final ObjectSet<Tile> tmpTiles = new ObjectSet<>();
static int sleepingEntities = 0;
Tile tile;
Block block;
Array<Tile> proximity = new Array<>(8);
PowerModule power;
ItemModule items;
LiquidModule liquids;
ConsumeModule cons;
private float timeScale = 1f, timeScaleDuration;
private @Nullable SoundLoop sound;
private boolean sleeping;
private float sleepTime;
/** Sets this tile entity data to this tile, and adds it if necessary. */
@Override
public Tilec init(Tile tile, boolean shouldAdd){
this.tile = tile;
this.block = tile.block();
set(tile.drawx(), tile.drawy());
if(block.activeSound != Sounds.none){
sound = new SoundLoop(block.activeSound, block.activeSoundVolume);
}
health(block.health);
maxHealth(block.health);
timer(new Interval(block.timers));
if(shouldAdd){
add();
}
return this;
}
@Override
public void applyBoost(float intensity, float duration){
timeScale = Math.max(timeScale, intensity);
timeScaleDuration = Math.max(timeScaleDuration, duration);
}
@Override
public float timeScale(){
return timeScale;
}
@Override
public boolean consValid(){
return cons.valid();
}
@Override
public void consume(){
cons.trigger();
}
/** Scaled delta. */
@Override
public float delta(){
return Time.delta() * timeScale;
}
/** Base efficiency. If this entity has non-buffered power, returns the power %, otherwise returns 1. */
@Override
public float efficiency(){
return power != null && (block.consumes.has(ConsumeType.power) && !block.consumes.getPower().buffered) ? power.status : 1f;
}
/** Call when nothing is happening to the entity. This increments the internal sleep timer. */
@Override
public void sleep(){
sleepTime += Time.delta();
if(!sleeping && sleepTime >= timeToSleep){
remove();
sleeping = true;
sleepingEntities++;
}
}
/** Call when this entity is updating. This wakes it up. */
@Override
public void noSleep(){
sleepTime = 0f;
if(sleeping){
add();
sleeping = false;
sleepingEntities--;
}
}
/** Returns the version of this TileEntity IO code.*/
@Override
public byte version(){
return 0;
}
@Override
public boolean collide(Bulletc other){
return true;
}
@Override
public void collision(Bulletc other){
block.handleBulletHit(this, other);
}
//TODO Implement damage!
@Override
public void removeFromProximity(){
block.onProximityRemoved(tile);
Point2[] nearby = Edges.getEdges(block.size);
for(Point2 point : nearby){
Tile other = world.ltile(tile.x + point.x, tile.y + point.y);
//remove this tile from all nearby tile's proximities
if(other != null){
other.block().onProximityUpdate(other);
if(other.entity != null){
other.entity.proximity().remove(tile, true);
}
}
}
}
@Override
public void updateProximity(){
tmpTiles.clear();
proximity.clear();
Point2[] nearby = Edges.getEdges(block.size);
for(Point2 point : nearby){
Tile other = world.ltile(tile.x + point.x, tile.y + point.y);
if(other == null) continue;
if(other.entity == null || !(other.interactable(tile.team()))) continue;
//add this tile to proximity of nearby tiles
if(!other.entity.proximity().contains(tile, true)){
other.entity.proximity().add(tile);
}
tmpTiles.add(other);
}
//using a set to prevent duplicates
for(Tile tile : tmpTiles){
proximity.add(tile);
}
block.onProximityAdded(tile);
block.onProximityUpdate(tile);
for(Tile other : tmpTiles){
other.block().onProximityUpdate(other);
}
}
@Override
public Array<Tile> proximity(){
return proximity;
}
/** Tile configuration. Defaults to 0. Used for block rebuilding. */
@Override
public int config(){
return 0;
}
@Override
public void remove(){
if(sound != null){
sound.stop();
}
}
@Override
public void killed(){
Events.fire(new BlockDestroyEvent(tile));
block.breakSound.at(tile);
block.onDestroyed(tile);
tile.remove();
}
@Override
public void update(){
timeScaleDuration -= Time.delta();
if(timeScaleDuration <= 0f || !block.canOverdrive){
timeScale = 1f;
}
if(sound != null){
sound.update(x(), y(), block.shouldActiveSound(tile));
}
if(block.idleSound != Sounds.none && block.shouldIdleSound(tile)){
loops.play(block.idleSound, this, block.idleSoundVolume);
}
block.update(tile);
if(liquids != null){
liquids.update();
}
if(cons != null){
cons.update();
}
if(power != null){
power.graph.update();
}
}
}
@Component
abstract static class PlayerComp implements UnitController, Entityc, Syncc, Timerc{
private static final Unitc noUnit = GenericUnitEntity.create();
@NonNull @ReadOnly Unitc unit = noUnit;
@ReadOnly Team team = Team.sharded;
String name = "noname";
@Nullable NetConnection con;
boolean admin, typing;
Color color = new Color();
float mouseX, mouseY;
@Nullable String lastText;
float textFadeTime;
public @Nullable Tilec closestCore(){
return state.teams.closestCore(x(), y(), team);
}
public void reset(){
team = state.rules.defaultTeam;
admin = typing = false;
lastText = null;
textFadeTime = 0f;
unit.controller(unit.type().createController());
unit = null;
}
public void update(){
if(!dead()){
x(unit.x());
y(unit.y());
unit.team(team);
}
textFadeTime -= Time.delta() / (60 * 5);
}
public void team(Team team){
if(unit != null){
unit.team(team);
}
}
public void clearUnit(){
unit(noUnit);
}
public Unitc unit(){
if(dead()){
//TODO remove
Log.err("WARNING: DEAD PLAYER UNIT ACCESSED");
new RuntimeException().printStackTrace();
}
return unit;
}
public void unit(Unitc unit){
if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead.");
this.unit = unit;
if(unit != noUnit){
unit.team(team);
}
}
boolean dead(){
return unit == noUnit;
}
String uuid(){
return con == null ? "[LOCAL]" : con.uuid;
}
String usid(){
return con == null ? "[LOCAL]" : con.usid;
}
void kick(KickReason reason){
con.kick(reason);
}
void kick(String reason){
con.kick(reason);
}
void drawName(){
BitmapFont font = Fonts.def;
GlyphLayout layout = Pools.obtain(GlyphLayout.class, GlyphLayout::new);
final float nameHeight = 11;
final float textHeight = 15;
boolean ints = font.usesIntegerPositions();
font.setUseIntegerPositions(false);
font.getData().setScale(0.25f / Scl.scl(1f));
layout.setText(font, name);
if(!isLocal()){
Draw.color(0f, 0f, 0f, 0.3f);
Fill.rect(unit.x(), unit.y() + nameHeight - layout.height / 2, layout.width + 2, layout.height + 3);
Draw.color();
font.setColor(color);
font.draw(name, unit.x(), unit.y() + nameHeight, 0, Align.center, false);
if(admin){
float s = 3f;
Draw.color(color.r * 0.5f, color.g * 0.5f, color.b * 0.5f, 1f);
Draw.rect(Icon.adminSmall.getRegion(), unit.x() + layout.width / 2f + 2 + 1, unit.y() + nameHeight - 1.5f, s, s);
Draw.color(color);
Draw.rect(Icon.adminSmall.getRegion(), unit.x() + layout.width / 2f + 2 + 1, unit.y() + nameHeight - 1f, s, s);
}
}
if(Core.settings.getBool("playerchat") && ((textFadeTime > 0 && lastText != null) || typing)){
String text = textFadeTime <= 0 || lastText == null ? "[LIGHT_GRAY]" + Strings.animated(Time.time(), 4, 15f, ".") : lastText;
float width = 100f;
float visualFadeTime = 1f - Mathf.curve(1f - textFadeTime, 0.9f);
font.setColor(1f, 1f, 1f, textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime);
layout.setText(font, text, Color.white, width, Align.bottom, true);
Draw.color(0f, 0f, 0f, 0.3f * (textFadeTime <= 0 || lastText == null ? 1f : visualFadeTime));
Fill.rect(unit.x(), unit.y() + textHeight + layout.height - layout.height/2f, layout.width + 2, layout.height + 3);
font.draw(text, unit.x() - width/2f, unit.y() + textHeight + layout.height, width, Align.center, true);
}
Draw.reset();
Pools.free(layout);
font.getData().setScale(1f);
font.setColor(Color.white);
font.setUseIntegerPositions(ints);
}
void sendMessage(String text){
if(isLocal()){
if(ui != null){
ui.chatfrag.addMessage(text, null);
}
}else{
Call.sendMessage(con, text, null, null);
}
}
void sendMessage(String text, Playerc from){
sendMessage(text, from, NetClient.colorizeName(from.id(), from.name()));
}
void sendMessage(String text, Playerc from, String fromName){
if(isLocal()){
if(ui != null){
ui.chatfrag.addMessage(text, fromName);
}
}else{
Call.sendMessage(con, text, fromName, from);
}
}
PlayerInfo getInfo(){
if(isLocal()){
throw new IllegalArgumentException("Local players cannot be traced and do not have info.");
}else{
return netServer.admins.getInfo(uuid());
}
}
}
@Component
abstract class TeamComp implements Posc{
transient float x, y;
Team team = Team.sharded;
public @Nullable Tilec closestCore(){
return state.teams.closestCore(x, y, team);
}
}
@Component
abstract static class WeaponsComp implements Teamc, Posc, Rotc{
transient float x, y, rotation;
/** 1 */
static final int[] one = {1};
/** minimum cursor distance from player, fixes 'cross-eyed' shooting */
static final float minAimDst = 20f;
/** temporary weapon sequence number */
static int sequenceNum = 0;
/** weapon mount array, never null */
@ReadOnly WeaponMount[] mounts = {};
void setupWeapons(UnitDef def){
mounts = new WeaponMount[def.weapons.size];
for(int i = 0; i < mounts.length; i++){
mounts[i] = new WeaponMount(def.weapons.get(i));
}
}
/** Aim at something. This will make all mounts point at it. */
void aim(Unitc unit, float x, float y){
Tmp.v1.set(x, y).sub(this.x, this.y);
if(Tmp.v1.len() < minAimDst) Tmp.v1.setLength(minAimDst);
x = Tmp.v1.x + this.x;
y = Tmp.v1.y + this.y;
for(WeaponMount mount : mounts){
mount.aimX = x;
mount.aimY = y;
}
}
/** Update shooting and rotation for this unit. */
@Override
public void update(){
for(WeaponMount mount : mounts){
Weapon weapon = mount.weapon;
mount.reload -= Time.delta();
float rotation = this.rotation - 90;
//rotate if applicable
if(weapon.rotate){
float axisXOffset = weapon.mirror ? 0f : weapon.x;
float axisX = this.x + Angles.trnsx(rotation, axisXOffset, weapon.y),
axisY = this.y + Angles.trnsy(rotation, axisXOffset, weapon.y);
mount.rotation = Angles.moveToward(mount.rotation, Angles.angle(axisX, axisY, mount.aimX, mount.aimY), weapon.rotateSpeed);
}
//shoot if applicable
//TODO only shoot if angle is reached, don't shoot inaccurately
if(mount.reload <= 0){
for(int i : (weapon.mirror && !weapon.alternate ? Mathf.signs : one)){
i *= Mathf.sign(weapon.flipped) * Mathf.sign(mount.side);
//m a t h
float weaponRotation = rotation + (weapon.rotate ? mount.rotation : 0);
float mountX = this.x + Angles.trnsx(rotation, weapon.x * i, weapon.y),
mountY = this.y + Angles.trnsy(rotation, weapon.x * i, weapon.y);
float shootX = mountX + Angles.trnsx(weaponRotation, weapon.shootX * i, weapon.shootY),
shootY = mountY + Angles.trnsy(weaponRotation, weapon.shootX * i, weapon.shootY);
float shootAngle = weapon.rotate ? weaponRotation : Angles.angle(shootX, shootY, mount.aimX, mount.aimY);
shoot(weapon, shootX, shootY, shootAngle);
}
mount.side = !mount.side;
mount.reload = weapon.reload;
}
}
}
/** Draw weapon mounts. */
void draw(){
for(WeaponMount mount : mounts){
Weapon weapon = mount.weapon;
for(int i : (weapon.mirror ? Mathf.signs : one)){
i *= Mathf.sign(weapon.flipped);
float rotation = this.rotation - 90 + (weapon.rotate ? mount.rotation : 0);
float trY = weapon.y - (mount.reload / weapon.reload * weapon.recoil) * (weapon.alternate ? Mathf.num(i == Mathf.sign(mount.side)) : 1);
float width = i > 0 ? -weapon.region.getWidth() : weapon.region.getWidth();
Draw.rect(weapon.region,
x + Angles.trnsx(rotation, weapon.x * i, trY),
y + Angles.trnsy(rotation, weapon.x * i, trY),
width * Draw.scl,
weapon.region.getHeight() * Draw.scl,
rotation - 90);
}
}
}
private void shoot(Weapon weapon, float x, float y, float rotation){
float baseX = this.x, baseY = this.y;
weapon.shootSound.at(x, y, Mathf.random(0.8f, 1.0f));
sequenceNum = 0;
if(weapon.shotDelay > 0.01f){
Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> {
Time.run(sequenceNum * weapon.shotDelay, () -> bullet(weapon, x + this.x - baseX, y + this.y - baseY, f + Mathf.range(weapon.inaccuracy)));
sequenceNum++;
});
}else{
Angles.shotgun(weapon.shots, weapon.spacing, rotation, f -> bullet(weapon, x, y, f + Mathf.range(weapon.inaccuracy)));
}
BulletType ammo = weapon.bullet;
Tmp.v1.trns(rotation + 180f, ammo.recoil);
if(this instanceof Velc){
//TODO apply force?
((Velc)this).vel().add(Tmp.v1);
}
Tmp.v1.trns(rotation, 3f);
boolean parentize = ammo.keepVelocity;
Effects.shake(weapon.shake, weapon.shake, x, y);
weapon.ejectEffect.at(x, y, rotation);
ammo.shootEffect.at(x + Tmp.v1.x, y + Tmp.v1.y, rotation, parentize ? this : null);
ammo.smokeEffect.at(x + Tmp.v1.x, y + Tmp.v1.y, rotation, parentize ? this : null);
}
private void bullet(Weapon weapon, float x, float y, float angle){
Tmp.v1.trns(angle, 3f);
weapon.bullet.create(this, team(), x + Tmp.v1.x, y + Tmp.v1.y, angle, (1f - weapon.velocityRnd) + Mathf.random(weapon.velocityRnd));
}
}
@Component
abstract static class DrawShadowComp implements Drawc, Rotc, Flyingc{
static final float shadowTX = -12, shadowTY = -13, shadowColor = Color.toFloatBits(0, 0, 0, 0.22f);
transient float x, y, rotation;
abstract TextureRegion getShadowRegion();
void drawShadow(){
if(!isGrounded()){
Draw.color(shadowColor);
Draw.rect(getShadowRegion(), x + shadowTX * elevation(), y + shadowTY * elevation(), rotation - 90);
Draw.color();
}
}
}
@Component
abstract class DrawItemsComp implements Drawc, Itemsc, Posc, Rotc{
transient float x, y, rotation;
float itemTime;
//drawn after base
@Override
@MethodPriority(3)
public void draw(){
boolean number = isLocal();
itemTime = Mathf.lerpDelta(itemTime, Mathf.num(hasItem()), 0.05f);
//draw back items
if(itemTime > 0.01f){
float backTrns = 5f;
float size = (itemSize + Mathf.absin(Time.time(), 5f, 1f)) * itemTime;
Draw.mixcol(Pal.accent, Mathf.absin(Time.time(), 5f, 0.5f));
Draw.rect(item().icon(Cicon.medium),
x + Angles.trnsx(rotation + 180f, backTrns),
y + Angles.trnsy(rotation + 180f, backTrns),
size, size, rotation);
Draw.mixcol();
Lines.stroke(1f, Pal.accent);
Lines.circle(
x + Angles.trnsx(rotation + 180f, backTrns),
y + Angles.trnsy(rotation + 180f, backTrns),
(3f + Mathf.absin(Time.time(), 5f, 1f)) * itemTime);
if(isLocal()){
Fonts.outline.draw(stack().amount + "",
x + Angles.trnsx(rotation + 180f, backTrns),
y + Angles.trnsy(rotation + 180f, backTrns) - 3,
Pal.accent, 0.25f * itemTime / Scl.scl(1f), false, Align.center
);
}
Draw.reset();
}
}
}
@Component
abstract class DecalComp implements Drawc, Timedc, Rotc, Posc{
Color color = new Color(1, 1, 1, 1);
TextureRegion region;
@Override
public void draw(){
Draw.color(color);
Draw.rect(region, x(), y(), rotation());
Draw.color();
}
@Override
public float clipSize(){
return region.getWidth()*2;
}
}
@Component
abstract class DrawLightComp implements Drawc{
void drawLight(){}
}
@Component
abstract class DrawOverComp implements Drawc{
void drawOver(){}
}
@Component
abstract class DrawUnderComp implements Drawc{
void drawUnder(){}
}
@Component
abstract class DrawComp{
abstract float clipSize();
void draw(){
}
}
@Component
abstract class SyncComp implements Posc{
transient float x, y;
Interpolator interpolator = new Interpolator();
void setNet(float x, float y){
set(x, y);
//TODO change interpolator API
interpolator.target.set(x, y);
interpolator.last.set(x, y);
interpolator.pos.set(0, 0);
interpolator.updateSpacing = 16;
interpolator.lastUpdated = 0;
}
@Override
public void update(){
if(Vars.net.client() && !isLocal()){
interpolate();
}
}
void interpolate(){
interpolator.update();
x = interpolator.pos.x;
y = interpolator.pos.y;
}
}
@Component
abstract class BoundedComp implements Velc, Posc, Healthc, Flyingc{
static final float warpDst = 180f;
transient float x, y;
transient Vec2 vel;
@Override
public void update(){
//repel unit out of bounds
if(x < 0) vel.x += (-x/warpDst);
if(y < 0) vel.y += (-y/warpDst);
if(x > world.unitWidth()) vel.x -= (x - world.unitWidth())/warpDst;
if(y > world.unitHeight()) vel.y -= (y - world.unitHeight())/warpDst;
//clamp position if not flying
if(isGrounded()){
x = Mathf.clamp(x, 0, world.width() * tilesize - tilesize);
y = Mathf.clamp(y, 0, world.height() * tilesize - tilesize);
}
//kill when out of bounds
if(x < -finalWorldBounds || y < -finalWorldBounds || x >= world.width() * tilesize + finalWorldBounds || y >= world.height() * tilesize + finalWorldBounds){
kill();
}
}
}
@Component
abstract class PosComp implements Position{
float x, y;
void set(float x, float y){
this.x = x;
this.y = y;
}
void trns(float x, float y){
set(this.x + x, this.y + y);
}
int tileX(){
return Vars.world.toTile(x);
}
int tileY(){
return Vars.world.toTile(y);
}
/** Returns air if this unit is on a non-air top block. */
public Floor floorOn(){
Tile tile = tileOn();
return tile == null || tile.block() != Blocks.air ? (Floor)Blocks.air : tile.floor();
}
public @Nullable Tile tileOn(){
return world.tileWorld(x, y);
}
@Override
public float getX(){
return x;
}
@Override
public float getY(){
return y;
}
}
@Component
static abstract class MinerComp implements Itemsc, Posc, Teamc, Rotc{
static float miningRange = 70f;
transient float x, y, rotation;
@Nullable Tile mineTile;
abstract boolean canMine(Item item);
abstract float miningSpeed();
abstract boolean offloadImmediately();
boolean mining(){
return mineTile != null;
}
void updateMining(){
Tilec core = closestCore();
if(core != null && mineTile != null && mineTile.drop() != null && !acceptsItem(mineTile.drop()) && dst(core) < mineTransferRange){
int accepted = core.tile().block().acceptStack(item(), stack().amount, core.tile(), this);
if(accepted > 0){
Call.transferItemTo(item(), accepted,
mineTile.worldx() + Mathf.range(tilesize / 2f),
mineTile.worldy() + Mathf.range(tilesize / 2f), core.tile());
clearItem();
}
}
if(mineTile == null || core == null || mineTile.block() != Blocks.air || dst(mineTile.worldx(), mineTile.worldy()) > miningRange
|| mineTile.drop() == null || !acceptsItem(mineTile.drop()) || !canMine(mineTile.drop())){
mineTile = null;
}else{
Item item = mineTile.drop();
rotation(Mathf.slerpDelta(rotation(), angleTo(mineTile.worldx(), mineTile.worldy()), 0.4f));
if(Mathf.chance(Time.delta() * (0.06 - item.hardness * 0.01) * miningSpeed())){
if(dst(core) < mineTransferRange && core.tile().block().acceptStack(item, 1, core.tile(), this) == 1 && offloadImmediately()){
Call.transferItemTo(item, 1,
mineTile.worldx() + Mathf.range(tilesize / 2f),
mineTile.worldy() + Mathf.range(tilesize / 2f), core.tile());
}else if(acceptsItem(item)){
//this is clientside, since items are synced anyway
InputHandler.transferItemToUnit(item,
mineTile.worldx() + Mathf.range(tilesize / 2f),
mineTile.worldy() + Mathf.range(tilesize / 2f),
this);
}
}
if(Mathf.chance(0.06 * Time.delta())){
Fx.pulverizeSmall.at(mineTile.worldx() + Mathf.range(tilesize / 2f), mineTile.worldy() + Mathf.range(tilesize / 2f), 0f, item.color);
}
}
}
void drawOver(){
if(!mining()) return;
float focusLen = 4f + Mathf.absin(Time.time(), 1.1f, 0.5f);
float swingScl = 12f, swingMag = tilesize / 8f;
float flashScl = 0.3f;
float px = x + Angles.trnsx(rotation, focusLen);
float py = y + Angles.trnsy(rotation, focusLen);
float ex = mineTile.worldx() + Mathf.sin(Time.time() + 48, swingScl, swingMag);
float ey = mineTile.worldy() + Mathf.sin(Time.time() + 48, swingScl + 2f, swingMag);
Draw.color(Color.lightGray, Color.white, 1f - flashScl + Mathf.absin(Time.time(), 0.5f, flashScl));
Drawf.laser(Core.atlas.find("minelaser"), Core.atlas.find("minelaser-end"), px, py, ex, ey, 0.75f);
//TODO hack?
if(isLocal()){
Lines.stroke(1f, Pal.accent);
Lines.poly(mineTile.worldx(), mineTile.worldy(), 4, tilesize / 2f * Mathf.sqrt2, Time.time());
}
Draw.color();
}
}
@Component
abstract static class BuilderComp implements Unitc{
static final float placeDistance = 220f;
static final Vec2[] tmptr = new Vec2[]{new Vec2(), new Vec2(), new Vec2(), new Vec2()};
transient float x, y, rotation;
@ReadOnly Queue<BuildRequest> requests = new Queue<>();
float buildSpeed = 1f;
//boolean building;
void updateBuilding(){
float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : placeDistance;
Iterator<BuildRequest> it = requests.iterator();
while(it.hasNext()){
BuildRequest req = it.next();
Tile tile = world.tile(req.x, req.y);
if(tile == null || (req.breaking && tile.block() == Blocks.air) || (!req.breaking && (tile.rotation() == req.rotation || !req.block.rotate) && tile.block() == req.block)){
it.remove();
}
}
Tilec core = closestCore();
//nothing to build.
if(buildRequest() == null) return;
//find the next build request
if(requests.size > 1){
int total = 0;
BuildRequest req;
while((dst((req = buildRequest()).tile()) > finalPlaceDst || shouldSkip(req, core)) && total < requests.size){
requests.removeFirst();
requests.addLast(req);
total++;
}
}
BuildRequest current = buildRequest();
if(dst(current.tile()) > finalPlaceDst) return;
Tile tile = world.tile(current.x, current.y);
if(!(tile.block() instanceof BuildBlock)){
if(!current.initialized && !current.breaking && Build.validPlace(team(), current.x, current.y, current.block, current.rotation)){
Build.beginPlace(team(), current.x, current.y, current.block, current.rotation);
}else if(!current.initialized && current.breaking && Build.validBreak(team(), current.x, current.y)){
Build.beginBreak(team(), current.x, current.y);
}else{
requests.removeFirst();
return;
}
}
if(tile.entity instanceof BuildEntity && !current.initialized){
Core.app.post(() -> Events.fire(new BuildSelectEvent(tile, team(), (Builderc)this, current.breaking)));
current.initialized = true;
}
//if there is no core to build with or no build entity, stop building!
if((core == null && !state.rules.infiniteResources) || !(tile.entity instanceof BuildEntity)){
return;
}
//otherwise, update it.
BuildEntity entity = tile.ent();
if(entity == null){
return;
}
if(dst(tile) <= finalPlaceDst){
rotation = Mathf.slerpDelta(rotation, angleTo(entity), 0.4f);
}
if(current.breaking){
entity.deconstruct(this, core, 1f / entity.buildCost * Time.delta() * buildSpeed * state.rules.buildSpeedMultiplier);
}else{
if(entity.construct(this, core, 1f / entity.buildCost * Time.delta() * buildSpeed * state.rules.buildSpeedMultiplier, current.hasConfig)){
if(current.hasConfig){
Call.onTileConfig(null, tile, current.config);
}
}
}
current.stuck = Mathf.equal(current.progress, entity.progress);
current.progress = entity.progress;
}
/** Draw all current build requests. Does not draw the beam effect, only the positions. */
void drawBuildRequests(){
for(BuildRequest request : requests){
if(request.progress > 0.01f || (buildRequest() == request && request.initialized && (dst(request.x * tilesize, request.y * tilesize) <= placeDistance || state.isEditor()))) continue;
request.animScale = 1f;
if(request.breaking){
control.input.drawBreaking(request);
}else{
request.block.drawRequest(request, control.input.allRequests(),
Build.validPlace(team(), request.x, request.y, request.block, request.rotation) || control.input.requestMatches(request));
}
}
Draw.reset();
}
/** @return whether this request should be skipped, in favor of the next one. */
boolean shouldSkip(BuildRequest request, @Nullable Tilec core){
//requests that you have at least *started* are considered
if(state.rules.infiniteResources || request.breaking || !request.initialized || core == null) return false;
return request.stuck && !core.items().has(request.block.requirements);
}
void removeBuild(int x, int y, boolean breaking){
//remove matching request
int idx = requests.indexOf(req -> req.breaking == breaking && req.x == x && req.y == y);
if(idx != -1){
requests.removeIndex(idx);
}
}
/** Return whether this builder's place queue contains items. */
boolean isBuilding(){
return requests.size != 0;
}
/** Clears the placement queue. */
void clearBuilding(){
requests.clear();
}
/** Add another build requests to the tail of the queue, if it doesn't exist there yet. */
void addBuild(BuildRequest place){
addBuild(place, true);
}
/** Add another build requests to the queue, if it doesn't exist there yet. */
void addBuild(BuildRequest place, boolean tail){
BuildRequest replace = null;
for(BuildRequest request : requests){
if(request.x == place.x && request.y == place.y){
replace = request;
break;
}
}
if(replace != null){
requests.remove(replace);
}
Tile tile = world.tile(place.x, place.y);
if(tile != null && tile.entity instanceof BuildEntity){
place.progress = tile.<BuildEntity>ent().progress;
}
if(tail){
requests.addLast(place);
}else{
requests.addFirst(place);
}
}
/** Return the build requests currently active, or the one at the top of the queue.*/
@Nullable BuildRequest buildRequest(){
return requests.size == 0 ? null : requests.first();
}
void drawOver(){
if(!isBuilding()) return;
BuildRequest request = buildRequest();
Tile tile = world.tile(request.x, request.y);
if(dst(tile) > placeDistance && !state.isEditor()){
return;
}
Lines.stroke(1f, Pal.accent);
float focusLen = 3.8f + Mathf.absin(Time.time(), 1.1f, 0.6f);
float px = x + Angles.trnsx(rotation, focusLen);
float py = y + Angles.trnsy(rotation, focusLen);
float sz = Vars.tilesize * tile.block().size / 2f;
float ang = angleTo(tile);
tmptr[0].set(tile.drawx() - sz, tile.drawy() - sz);
tmptr[1].set(tile.drawx() + sz, tile.drawy() - sz);
tmptr[2].set(tile.drawx() - sz, tile.drawy() + sz);
tmptr[3].set(tile.drawx() + sz, tile.drawy() + sz);
Arrays.sort(tmptr, Structs.comparingFloat(vec -> Angles.angleDist(angleTo(vec), ang)));
float x1 = tmptr[0].x, y1 = tmptr[0].y,
x3 = tmptr[1].x, y3 = tmptr[1].y;
Draw.alpha(1f);
Lines.line(px, py, x1, y1);
Lines.line(px, py, x3, y3);
Fill.circle(px, py, 1.6f + Mathf.absin(Time.time(), 0.8f, 1.5f));
Draw.color();
}
}
@Component
abstract class ShielderComp implements Damagec, Teamc, Posc{
void absorb(){
}
}
@Component
abstract class ItemsComp implements Posc{
@ReadOnly ItemStack stack = new ItemStack();
abstract int itemCapacity();
@Override
public void update(){
stack.amount = Mathf.clamp(stack.amount, 0, itemCapacity());
}
Item item(){
return stack.item;
}
void clearItem(){
stack.amount = 0;
}
boolean acceptsItem(Item item){
return !hasItem() || item == stack.item && stack.amount + 1 <= itemCapacity();
}
boolean hasItem(){
return stack.amount > 0;
}
void addItem(Item item){
addItem(item, 1);
}
void addItem(Item item, int amount){
stack.amount = stack.item == item ? stack.amount + amount : amount;
stack.item = item;
stack.amount = Mathf.clamp(stack.amount, 0, itemCapacity());
}
int maxAccepted(Item item){
return stack.item != item && stack.amount > 0 ? 0 : itemCapacity() - stack.amount;
}
}
@Component
abstract class MassComp implements Velc{
float mass = 1f;
public void applyImpulse(float x, float y){
vel().add(x / mass, y / mass);
}
}
@Component
abstract class EffectComp implements Posc, Drawc, Timedc, Rotc{
Effect effect;
Color color = new Color(Color.white);
Object data;
@Override
public void draw(){
effect.render(id(), color, time(), rotation(), x(), y(), data);
}
@Override
public float clipSize(){
return effect.size;
}
}
@Component
abstract class VelComp implements Posc{
transient float x, y;
final Vec2 vel = new Vec2();
float drag = 0f;
@Override
public void update(){
//TODO handle solidity
x += vel.x;
y += vel.y;
vel.scl(1f - drag * Time.delta());
}
}
@Component
abstract class HitboxComp implements Posc, QuadTreeObject{
transient float x, y;
float hitSize;
float lastX, lastY;
@Override
public void update(){
}
void updateLastPosition(){
lastX = x;
lastY = y;
}
void collision(Hitboxc other, float x, float y){
}
float deltaX(){
return x - lastX;
}
float deltaY(){
return y - lastY;
}
boolean collides(Hitboxc other){
return Intersector.overlapsRect(x - hitSize/2f, y - hitSize/2f, hitSize, hitSize,
other.x() - other.hitSize()/2f, other.y() - other.hitSize()/2f, other.hitSize(), other.hitSize());
}
@Override
public void hitbox(Rect rect){
rect.setCentered(x, y, hitSize, hitSize);
}
public void hitboxTile(Rect rect){
float scale = 0.6f;
rect.setCentered(x, y, hitSize * scale, hitSize * scale);
}
}
@Component
abstract class StatusComp implements Posc, Flyingc{
private Array<StatusEntry> statuses = new Array<>();
private Bits applied = new Bits(content.getBy(ContentType.status).size);
@ReadOnly float speedMultiplier;
@ReadOnly float damageMultiplier;
@ReadOnly float armorMultiplier;
/** @return damage taken based on status armor multipliers */
float getShieldDamage(float amount){
return amount * Mathf.clamp(1f - armorMultiplier / 100f);
}
void apply(StatusEffect effect, float duration){
if(effect == StatusEffects.none || effect == null || isImmune(effect)) return; //don't apply empty or immune effects
if(statuses.size > 0){
//check for opposite effects
for(StatusEntry entry : statuses){
//extend effect
if(entry.effect == effect){
entry.time = Math.max(entry.time, duration);
return;
}else if(entry.effect.reactsWith(effect)){ //find opposite
StatusEntry.tmp.effect = entry.effect;
entry.effect.getTransition((Unitc)this, effect, entry.time, duration, StatusEntry.tmp);
entry.time = StatusEntry.tmp.time;
if(StatusEntry.tmp.effect != entry.effect){
entry.effect = StatusEntry.tmp.effect;
}
//stop looking when one is found
return;
}
}
}
//otherwise, no opposites found, add direct effect
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
entry.set(effect, duration);
statuses.add(entry);
}
boolean isBoss(){
return hasEffect(StatusEffects.boss);
}
boolean isImmune(StatusEffect effect){
return false;
}
Color statusColor(){
if(statuses.size == 0){
return Tmp.c1.set(Color.white);
}
float r = 0f, g = 0f, b = 0f;
for(StatusEntry entry : statuses){
r += entry.effect.color.r;
g += entry.effect.color.g;
b += entry.effect.color.b;
}
return Tmp.c1.set(r / statuses.size, g / statuses.size, b / statuses.size, 1f);
}
@Override
public void update(){
Floor floor = floorOn();
if(isGrounded() && floor.status != null){
//apply effect
apply(floor.status, floor.statusDuration);
}
applied.clear();
speedMultiplier = damageMultiplier = armorMultiplier = 1f;
if(statuses.isEmpty()) return;
statuses.eachFilter(entry -> {
entry.time = Math.max(entry.time - Time.delta(), 0);
applied.set(entry.effect.id);
if(entry.time <= 0){
Pools.free(entry);
return true;
}else{
speedMultiplier *= entry.effect.speedMultiplier;
armorMultiplier *= entry.effect.armorMultiplier;
damageMultiplier *= entry.effect.damageMultiplier;
//TODO ugly casting
entry.effect.update((mindustry.gen.Unitc)this, entry.time);
}
return false;
});
}
boolean hasEffect(StatusEffect effect){
return applied.get(effect.id);
}
void writeSave(DataOutput stream) throws IOException{
stream.writeByte(statuses.size);
for(StatusEntry entry : statuses){
stream.writeByte(entry.effect.id);
stream.writeFloat(entry.time);
}
}
void readSave(DataInput stream, byte version) throws IOException{
for(StatusEntry effect : statuses){
Pools.free(effect);
}
statuses.clear();
byte amount = stream.readByte();
for(int i = 0; i < amount; i++){
byte id = stream.readByte();
float time = stream.readFloat();
StatusEntry entry = Pools.obtain(StatusEntry.class, StatusEntry::new);
entry.set(content.getByID(ContentType.status, id), time);
statuses.add(entry);
}
}
}
@Component
@BaseComponent
abstract class EntityComp{
private boolean added;
int id;
boolean isAdded(){
return added;
}
void init(){}
void update(){}
void remove(){
added = false;
}
void add(){
added = true;
}
boolean isLocal(){
return ((Object)this) == player || ((Object)this) instanceof Unitc && ((Unitc)((Object)this)).controller() == player;
}
<T> T as(Class<T> type){
return (T)this;
}
@InternalImpl
abstract int classId();
void read(DataInput input) throws IOException{
//TODO dynamic io
}
void write(DataOutput output) throws IOException{
//TODO dynamic io
}
}
}