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 tmpTiles = new ObjectSet<>(); static int sleepingEntities = 0; Tile tile; Block block; Array 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 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 requests = new Queue<>(); float buildSpeed = 1f; //boolean building; void updateBuilding(){ float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : placeDistance; Iterator 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.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 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 as(Class 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 } } }