diff --git a/annotations/src/main/java/mindustry/annotations/Annotations.java b/annotations/src/main/java/mindustry/annotations/Annotations.java index 4f718807e3..f880f25377 100644 --- a/annotations/src/main/java/mindustry/annotations/Annotations.java +++ b/annotations/src/main/java/mindustry/annotations/Annotations.java @@ -5,6 +5,24 @@ import java.lang.annotation.*; public class Annotations{ //region entity interfaces + /** Indicates that a component field is read-only. */ + @Target({ElementType.METHOD}) + @Retention(RetentionPolicy.SOURCE) + public @interface Render{ + RenderLayer value(); + } + + public enum RenderLayer{ + floor, + groundShadows, + ground, + flyingShadows, + flying, + bullets, + effects, + names, + } + /** Indicates that a component field is read-only. */ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) diff --git a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java index 7e149036f0..4b46cecdb7 100644 --- a/annotations/src/main/java/mindustry/annotations/BaseProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/BaseProcessor.java @@ -3,6 +3,7 @@ package mindustry.annotations; import arc.files.*; import arc.struct.Array; import arc.util.*; +import arc.util.Log.*; import com.squareup.javapoet.*; import com.sun.source.util.*; import mindustry.annotations.util.*; @@ -151,6 +152,10 @@ public abstract class BaseProcessor extends AbstractProcessor{ elementu = env.getElementUtils(); filer = env.getFiler(); messager = env.getMessager(); + + if(System.getProperty("debug") == null){ + Log.setLogLevel(LogLevel.err); + } } @Override @@ -159,7 +164,7 @@ public abstract class BaseProcessor extends AbstractProcessor{ if(rootDirectory == null){ try{ String path = Fi.get(filer.getResource(StandardLocation.CLASS_OUTPUT, "no", "no") - .toUri().toURL().toString().substring(System.getProperty("os.name").contains("Windows") ? 6 : "file:".length())) + .toUri().toURL().toString().substring(OS.isWindows ? 6 : "file:".length())) .parent().parent().parent().parent().parent().parent().parent().toString().replace("%20", " "); rootDirectory = Fi.get(path); }catch(IOException e){ diff --git a/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java b/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java index b93cc53116..b2a810d75e 100644 --- a/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java +++ b/annotations/src/main/java/mindustry/annotations/impl/EntityProcess.java @@ -19,6 +19,7 @@ import java.lang.annotation.*; @SupportedAnnotationTypes({ "mindustry.annotations.Annotations.EntityDef", +"mindustry.annotations.Annotations.GroupDef", "mindustry.annotations.Annotations.EntityInterface", "mindustry.annotations.Annotations.BaseComponent" }) @@ -64,7 +65,8 @@ public class EntityProcess extends BaseProcessor{ //create component interfaces for(Stype component : allComponents){ - TypeSpec.Builder inter = TypeSpec.interfaceBuilder(interfaceName(component)).addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class); + TypeSpec.Builder inter = TypeSpec.interfaceBuilder(interfaceName(component)) + .addModifiers(Modifier.PUBLIC).addAnnotation(EntityInterface.class); //implement extra interfaces these components may have, e.g. position for(Stype extraInterface : component.interfaces().select(i -> !isCompInterface(i))){ diff --git a/annotations/src/main/resources/classids.properties b/annotations/src/main/resources/classids.properties index 462b3bb974..6e47fcb5e7 100644 --- a/annotations/src/main/resources/classids.properties +++ b/annotations/src/main/resources/classids.properties @@ -1,9 +1,9 @@ #Maps entity names to IDs. Autogenerated. -mindustry.entities.def.EntityDefs.DecalDef=1 -mindustry.entities.def.EntityDefs.EffectDef=2 -mindustry.entities.def.EntityDefs.TileDef=3 -mindustry.entities.def.EntityDefs.GenericUnitDef=5 -mindustry.entities.def.EntityDefs.GenericBuilderDef=6 -mindustry.entities.def.EntityDefs.BulletDef=0 -mindustry.entities.def.EntityDefs.PlayerDef=4 \ No newline at end of file +mindustry.entities.def.AllEntities.GenericBuilderDef=6 +mindustry.entities.def.AllEntities.BulletDef=0 +mindustry.entities.def.AllEntities.PlayerDef=4 +mindustry.entities.def.AllEntities.GenericUnitDef=5 +mindustry.entities.def.AllEntities.EffectDef=2 +mindustry.entities.def.AllEntities.DecalDef=1 +mindustry.entities.def.AllEntities.TileDef=3 \ No newline at end of file diff --git a/core/src/mindustry/core/Renderer.java b/core/src/mindustry/core/Renderer.java index 91788a34be..3160b04ff3 100644 --- a/core/src/mindustry/core/Renderer.java +++ b/core/src/mindustry/core/Renderer.java @@ -240,6 +240,7 @@ public class Renderer implements ApplicationListener{ overlays.drawTop(); + render(RenderLayer.names); //TODO should use (draw) Groups.player.each(p -> !p.dead(), Playerc::drawName); diff --git a/core/src/mindustry/entities/EntityGroup.java b/core/src/mindustry/entities/EntityGroup.java index 6484a8f9f1..bb520e99e0 100644 --- a/core/src/mindustry/entities/EntityGroup.java +++ b/core/src/mindustry/entities/EntityGroup.java @@ -1,5 +1,6 @@ package mindustry.entities; +import arc.*; import arc.func.*; import arc.math.geom.*; import arc.struct.*; @@ -7,13 +8,14 @@ import mindustry.gen.*; import java.util.*; -import static mindustry.Vars.*; +import static mindustry.Vars.collisions; /** Represents a group of a certain type of entity.*/ @SuppressWarnings("unchecked") public class EntityGroup implements Iterable{ private final Array array; private final Array intersectArray = new Array<>(); + private final Rect viewport = new Rect(); private final Rect intersectRect = new Rect(); private IntMap map; private QuadTree tree; @@ -60,6 +62,17 @@ public class EntityGroup implements Iterable{ } } + public void draw(Cons cons){ + Core.camera.bounds(viewport); + + each(e -> { + Drawc draw = (Drawc)e; + if(viewport.overlaps(draw.x() - draw.clipSize()/2f, draw.y() - draw.clipSize()/2f, draw.clipSize(), draw.clipSize())){ + cons.get(e); + } + }); + } + public boolean useTree(){ return map != null; } diff --git a/core/src/mindustry/entities/def/EntityDefs.java b/core/src/mindustry/entities/def/AllEntities.java similarity index 59% rename from core/src/mindustry/entities/def/EntityDefs.java rename to core/src/mindustry/entities/def/AllEntities.java index 3c9122c808..764360adfb 100644 --- a/core/src/mindustry/entities/def/EntityDefs.java +++ b/core/src/mindustry/entities/def/AllEntities.java @@ -1,9 +1,8 @@ package mindustry.entities.def; import mindustry.annotations.Annotations.*; -import mindustry.entities.def.EntityComps.*; -class EntityDefs{ +class AllEntities{ @EntityDef({BulletComp.class, VelComp.class, TimedComp.class}) class BulletDef{} @@ -25,4 +24,34 @@ class EntityDefs{ @EntityDef({BuilderComp.class}) class GenericBuilderDef{} + + @GroupDef(EntityComp.class) + void all(){ + + } + + @GroupDef(PlayerComp.class) + void player(){ + + } + + @GroupDef(value = UnitComp.class, spatial = true) + void unit(){ + + } + + @GroupDef(TileComp.class) + void tile(){ + + } + + @GroupDef(DrawComp.class) + void drawer(){ + + } + + @GroupDef(SyncComp.class) + void sync(){ + + } } diff --git a/core/src/mindustry/entities/def/BoundedComp.java b/core/src/mindustry/entities/def/BoundedComp.java new file mode 100644 index 0000000000..da3da73fbf --- /dev/null +++ b/core/src/mindustry/entities/def/BoundedComp.java @@ -0,0 +1,36 @@ +package mindustry.entities.def; + +import arc.math.*; +import arc.math.geom.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +import static mindustry.Vars.*; + +@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(); + } + } +} diff --git a/core/src/mindustry/entities/def/BuilderComp.java b/core/src/mindustry/entities/def/BuilderComp.java new file mode 100644 index 0000000000..480449cea0 --- /dev/null +++ b/core/src/mindustry/entities/def/BuilderComp.java @@ -0,0 +1,229 @@ +package mindustry.entities.def; + +import arc.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.struct.Queue; +import arc.util.*; +import arc.util.ArcAnnotate.*; +import mindustry.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.units.*; +import mindustry.game.EventType.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.world.*; +import mindustry.world.blocks.*; +import mindustry.world.blocks.BuildBlock.*; + +import java.util.*; + +import static mindustry.Vars.*; + +@Component +abstract class BuilderComp implements Unitc{ + static final Vec2[] tmptr = new Vec2[]{new Vec2(), new Vec2(), new Vec2(), new Vec2()}; + + transient float x, y, rotation; + + Queue requests = new Queue<>(); + float buildSpeed = 1f; + //boolean building; + + void updateBuilding(){ + float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : buildingRange; + + 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) <= buildingRange || 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) > buildingRange && !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(); + } +} diff --git a/core/src/mindustry/entities/def/BulletComp.java b/core/src/mindustry/entities/def/BulletComp.java new file mode 100644 index 0000000000..ef4d1e3b03 --- /dev/null +++ b/core/src/mindustry/entities/def/BulletComp.java @@ -0,0 +1,121 @@ +package mindustry.entities.def; + +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.bullet.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/ChildComp.java b/core/src/mindustry/entities/def/ChildComp.java new file mode 100644 index 0000000000..c1d8f0d955 --- /dev/null +++ b/core/src/mindustry/entities/def/ChildComp.java @@ -0,0 +1,29 @@ +package mindustry.entities.def; + +import arc.util.ArcAnnotate.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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; + } + } +} diff --git a/core/src/mindustry/entities/def/DamageComp.java b/core/src/mindustry/entities/def/DamageComp.java new file mode 100644 index 0000000000..2ace4db324 --- /dev/null +++ b/core/src/mindustry/entities/def/DamageComp.java @@ -0,0 +1,8 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; + +@Component +abstract class DamageComp{ + abstract float damage(); +} diff --git a/core/src/mindustry/entities/def/DecalComp.java b/core/src/mindustry/entities/def/DecalComp.java new file mode 100644 index 0000000000..7b7d32a6ed --- /dev/null +++ b/core/src/mindustry/entities/def/DecalComp.java @@ -0,0 +1,25 @@ +package mindustry.entities.def; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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; + } + +} diff --git a/core/src/mindustry/entities/def/DrawComp.java b/core/src/mindustry/entities/def/DrawComp.java new file mode 100644 index 0000000000..fa5e06285b --- /dev/null +++ b/core/src/mindustry/entities/def/DrawComp.java @@ -0,0 +1,14 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class DrawComp implements Posc{ + + abstract float clipSize(); + + void draw(){ + + } +} diff --git a/core/src/mindustry/entities/def/DrawItemsComp.java b/core/src/mindustry/entities/def/DrawItemsComp.java new file mode 100644 index 0000000000..6988a27d08 --- /dev/null +++ b/core/src/mindustry/entities/def/DrawItemsComp.java @@ -0,0 +1,57 @@ +package mindustry.entities.def; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.ui.*; + +import static mindustry.Vars.itemSize; + +@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(); + } + } +} diff --git a/core/src/mindustry/entities/def/DrawLightComp.java b/core/src/mindustry/entities/def/DrawLightComp.java new file mode 100644 index 0000000000..5fd8cc02d2 --- /dev/null +++ b/core/src/mindustry/entities/def/DrawLightComp.java @@ -0,0 +1,9 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class DrawLightComp implements Drawc{ + void drawLight(){} +} diff --git a/core/src/mindustry/entities/def/DrawOverComp.java b/core/src/mindustry/entities/def/DrawOverComp.java new file mode 100644 index 0000000000..271072646d --- /dev/null +++ b/core/src/mindustry/entities/def/DrawOverComp.java @@ -0,0 +1,9 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class DrawOverComp implements Drawc{ + void drawOver(){} +} diff --git a/core/src/mindustry/entities/def/DrawShadowComp.java b/core/src/mindustry/entities/def/DrawShadowComp.java new file mode 100644 index 0000000000..67cffe3a56 --- /dev/null +++ b/core/src/mindustry/entities/def/DrawShadowComp.java @@ -0,0 +1,23 @@ +package mindustry.entities.def; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract 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(); + } + } +} diff --git a/core/src/mindustry/entities/def/DrawUnderComp.java b/core/src/mindustry/entities/def/DrawUnderComp.java new file mode 100644 index 0000000000..b8c45ccd2f --- /dev/null +++ b/core/src/mindustry/entities/def/DrawUnderComp.java @@ -0,0 +1,9 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class DrawUnderComp implements Drawc{ + void drawUnder(){} +} diff --git a/core/src/mindustry/entities/def/EffectComp.java b/core/src/mindustry/entities/def/EffectComp.java new file mode 100644 index 0000000000..2c9c1eb80c --- /dev/null +++ b/core/src/mindustry/entities/def/EffectComp.java @@ -0,0 +1,23 @@ +package mindustry.entities.def; + +import arc.graphics.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.*; +import mindustry.gen.*; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/EntityComp.java b/core/src/mindustry/entities/def/EntityComp.java new file mode 100644 index 0000000000..46e7fce919 --- /dev/null +++ b/core/src/mindustry/entities/def/EntityComp.java @@ -0,0 +1,60 @@ +package mindustry.entities.def; + +import arc.func.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +import java.io.*; + +import static mindustry.Vars.player; + +@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; + } + + boolean isNull(){ + return false; + } + + T as(Class type){ + return (T)this; + } + + T with(Cons cons){ + cons.get((T)this); + 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 + } +} diff --git a/core/src/mindustry/entities/def/EntityComps.java b/core/src/mindustry/entities/def/EntityComps.java deleted file mode 100644 index 7211590da2..0000000000 --- a/core/src/mindustry/entities/def/EntityComps.java +++ /dev/null @@ -1,1820 +0,0 @@ -package mindustry.entities.def; - -import arc.*; -import arc.func.*; -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; - controller(type.createController()); - 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{ - @NonNull @ReadOnly Unitc unit = Nulls.unit; - - @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 boolean isBuilder(){ - return unit instanceof Builderc; - } - - public boolean isMiner(){ - return unit instanceof Minerc; - } - - 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; - if(!dead()){ - unit.controller(unit.type().createController()); - unit = Nulls.unit; - } - } - - 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(Nulls.unit); - } - - public Unitc unit(){ - if(dead()){ - //TODO remove - Log.err("WARNING: DEAD PLAYER UNIT ACCESSED"); - new RuntimeException().printStackTrace(); - } - return unit; - } - - public Minerc miner(){ - return !(unit instanceof Minerc) ? Nulls.miner : (Minerc)unit; - } - - public Builderc builder(){ - return !(unit instanceof Builderc) ? Nulls.builder : (Builderc)unit; - } - - public void unit(Unitc unit){ - if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead."); - this.unit = unit; - if(unit != Nulls.unit){ - unit.team(team); - } - } - - boolean dead(){ - return unit.isNull(); - } - - 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{ - 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 Vec2[] tmptr = new Vec2[]{new Vec2(), new Vec2(), new Vec2(), new Vec2()}; - - transient float x, y, rotation; - - Queue requests = new Queue<>(); - float buildSpeed = 1f; - //boolean building; - - void updateBuilding(){ - float finalPlaceDst = state.rules.infiniteResources ? Float.MAX_VALUE : buildingRange; - - 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) <= buildingRange || 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) > buildingRange && !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; - } - - boolean isNull(){ - return false; - } - - T as(Class type){ - return (T)this; - } - - T with(Cons cons){ - cons.get((T)this); - 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 - } - } -} diff --git a/core/src/mindustry/entities/def/EntityGroupDefs.java b/core/src/mindustry/entities/def/EntityGroupDefs.java deleted file mode 100644 index 215b9724e6..0000000000 --- a/core/src/mindustry/entities/def/EntityGroupDefs.java +++ /dev/null @@ -1,37 +0,0 @@ -package mindustry.entities.def; - -import mindustry.annotations.Annotations.*; -import mindustry.entities.def.EntityComps.*; - -public class EntityGroupDefs{ - - @GroupDef(EntityComp.class) - void all(){ - - } - - @GroupDef(PlayerComp.class) - void player(){ - - } - - @GroupDef(value = UnitComp.class, spatial = true) - void unit(){ - - } - - @GroupDef(TileComp.class) - void tile(){ - - } - - @GroupDef(DrawComp.class) - void drawer(){ - - } - - @GroupDef(SyncComp.class) - void sync(){ - - } -} diff --git a/core/src/mindustry/entities/def/FlyingComp.java b/core/src/mindustry/entities/def/FlyingComp.java new file mode 100644 index 0000000000..81d5a3b99b --- /dev/null +++ b/core/src/mindustry/entities/def/FlyingComp.java @@ -0,0 +1,48 @@ +package mindustry.entities.def; + +import arc.math.*; +import arc.math.geom.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.world.blocks.*; + +import static mindustry.Vars.net; + +@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); + } + } +} diff --git a/core/src/mindustry/entities/def/HealthComp.java b/core/src/mindustry/entities/def/HealthComp.java new file mode 100644 index 0000000000..86b4127783 --- /dev/null +++ b/core/src/mindustry/entities/def/HealthComp.java @@ -0,0 +1,76 @@ +package mindustry.entities.def; + +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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(); + } +} diff --git a/core/src/mindustry/entities/def/HitboxComp.java b/core/src/mindustry/entities/def/HitboxComp.java new file mode 100644 index 0000000000..884da0dd48 --- /dev/null +++ b/core/src/mindustry/entities/def/HitboxComp.java @@ -0,0 +1,51 @@ +package mindustry.entities.def; + +import arc.math.geom.*; +import arc.math.geom.QuadTree.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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); + } +} diff --git a/core/src/mindustry/entities/def/ItemsComp.java b/core/src/mindustry/entities/def/ItemsComp.java new file mode 100644 index 0000000000..2cf27c79d9 --- /dev/null +++ b/core/src/mindustry/entities/def/ItemsComp.java @@ -0,0 +1,48 @@ +package mindustry.entities.def; + +import arc.math.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.type.*; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/LegsComp.java b/core/src/mindustry/entities/def/LegsComp.java new file mode 100644 index 0000000000..2ab7a115ba --- /dev/null +++ b/core/src/mindustry/entities/def/LegsComp.java @@ -0,0 +1,9 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class LegsComp implements Posc, Flyingc{ + float baseRotation; +} diff --git a/core/src/mindustry/entities/def/MassComp.java b/core/src/mindustry/entities/def/MassComp.java new file mode 100644 index 0000000000..e9f06aa9bc --- /dev/null +++ b/core/src/mindustry/entities/def/MassComp.java @@ -0,0 +1,13 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class MassComp implements Velc{ + float mass = 1f; + + public void applyImpulse(float x, float y){ + vel().add(x / mass, y / mass); + } +} diff --git a/core/src/mindustry/entities/def/MinerComp.java b/core/src/mindustry/entities/def/MinerComp.java new file mode 100644 index 0000000000..684af9fff2 --- /dev/null +++ b/core/src/mindustry/entities/def/MinerComp.java @@ -0,0 +1,100 @@ +package mindustry.entities.def; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import arc.util.ArcAnnotate.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.input.*; +import mindustry.type.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +@Component +abstract class MinerComp implements Itemsc, Posc, Teamc, Rotc{ + 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(); + } +} diff --git a/core/src/mindustry/entities/def/OwnerComp.java b/core/src/mindustry/entities/def/OwnerComp.java new file mode 100644 index 0000000000..e5e1f9d5ae --- /dev/null +++ b/core/src/mindustry/entities/def/OwnerComp.java @@ -0,0 +1,9 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +class OwnerComp{ + Entityc owner; +} diff --git a/core/src/mindustry/entities/def/PlayerComp.java b/core/src/mindustry/entities/def/PlayerComp.java new file mode 100644 index 0000000000..6122d074d0 --- /dev/null +++ b/core/src/mindustry/entities/def/PlayerComp.java @@ -0,0 +1,203 @@ +package mindustry.entities.def; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.scene.ui.layout.*; +import arc.util.*; +import arc.util.ArcAnnotate.*; +import arc.util.pooling.*; +import mindustry.annotations.Annotations.*; +import mindustry.core.*; +import mindustry.entities.units.*; +import mindustry.game.*; +import mindustry.gen.*; +import mindustry.net.*; +import mindustry.net.Administration.*; +import mindustry.net.Packets.*; +import mindustry.ui.*; + +import static mindustry.Vars.*; + +@Component +abstract class PlayerComp implements UnitController, Entityc, Syncc, Timerc{ + @NonNull + @ReadOnly Unitc unit = Nulls.unit; + + @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 boolean isBuilder(){ + return unit instanceof Builderc; + } + + public boolean isMiner(){ + return unit instanceof Minerc; + } + + 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; + if(!dead()){ + unit.controller(unit.type().createController()); + unit = Nulls.unit; + } + } + + 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(Nulls.unit); + } + + public Unitc unit(){ + if(dead()){ + //TODO remove + Log.err("WARNING: DEAD PLAYER UNIT ACCESSED"); + new RuntimeException().printStackTrace(); + } + return unit; + } + + public Minerc miner(){ + return !(unit instanceof Minerc) ? Nulls.miner : (Minerc)unit; + } + + public Builderc builder(){ + return !(unit instanceof Builderc) ? Nulls.builder : (Builderc)unit; + } + + public void unit(Unitc unit){ + if(unit == null) throw new IllegalArgumentException("Unit cannot be null. Use clearUnit() instead."); + this.unit = unit; + if(unit != Nulls.unit){ + unit.team(team); + } + } + + boolean dead(){ + return unit.isNull(); + } + + 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()); + } + } +} diff --git a/core/src/mindustry/entities/def/PosComp.java b/core/src/mindustry/entities/def/PosComp.java new file mode 100644 index 0000000000..11408b0edb --- /dev/null +++ b/core/src/mindustry/entities/def/PosComp.java @@ -0,0 +1,54 @@ +package mindustry.entities.def; + +import arc.math.geom.*; +import arc.util.ArcAnnotate.*; +import mindustry.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.world.*; +import mindustry.world.blocks.*; + +import static mindustry.Vars.world; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/RotComp.java b/core/src/mindustry/entities/def/RotComp.java new file mode 100644 index 0000000000..8740b78935 --- /dev/null +++ b/core/src/mindustry/entities/def/RotComp.java @@ -0,0 +1,17 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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]; + } + } +} diff --git a/core/src/mindustry/entities/def/ShielderComp.java b/core/src/mindustry/entities/def/ShielderComp.java new file mode 100644 index 0000000000..86a038d9fb --- /dev/null +++ b/core/src/mindustry/entities/def/ShielderComp.java @@ -0,0 +1,12 @@ +package mindustry.entities.def; + +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@Component +abstract class ShielderComp implements Damagec, Teamc, Posc{ + + void absorb(){ + + } +} diff --git a/core/src/mindustry/entities/def/StatusComp.java b/core/src/mindustry/entities/def/StatusComp.java new file mode 100644 index 0000000000..308792495c --- /dev/null +++ b/core/src/mindustry/entities/def/StatusComp.java @@ -0,0 +1,147 @@ +package mindustry.entities.def; + +import arc.graphics.*; +import arc.math.*; +import arc.struct.*; +import arc.util.*; +import arc.util.pooling.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.ctype.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.blocks.*; + +import java.io.*; + +import static mindustry.Vars.content; + +@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((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); + } + } +} diff --git a/core/src/mindustry/entities/def/SyncComp.java b/core/src/mindustry/entities/def/SyncComp.java new file mode 100644 index 0000000000..cbe2e6735f --- /dev/null +++ b/core/src/mindustry/entities/def/SyncComp.java @@ -0,0 +1,37 @@ +package mindustry.entities.def; + +import mindustry.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; +import mindustry.net.*; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/TeamComp.java b/core/src/mindustry/entities/def/TeamComp.java new file mode 100644 index 0000000000..c9c540634e --- /dev/null +++ b/core/src/mindustry/entities/def/TeamComp.java @@ -0,0 +1,20 @@ +package mindustry.entities.def; + +import arc.util.ArcAnnotate.*; +import mindustry.annotations.Annotations.*; +import mindustry.game.*; +import mindustry.gen.*; + +import static mindustry.Vars.state; + +@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); + } +} diff --git a/core/src/mindustry/entities/def/TileComp.java b/core/src/mindustry/entities/def/TileComp.java new file mode 100644 index 0000000000..697d7d44c7 --- /dev/null +++ b/core/src/mindustry/entities/def/TileComp.java @@ -0,0 +1,241 @@ +package mindustry.entities.def; + +import arc.*; +import arc.math.geom.*; +import arc.struct.*; +import arc.util.*; +import arc.util.ArcAnnotate.*; +import mindustry.annotations.Annotations.*; +import mindustry.game.*; +import mindustry.game.EventType.*; +import mindustry.gen.*; +import mindustry.world.*; +import mindustry.world.consumers.*; +import mindustry.world.modules.*; + +import static mindustry.Vars.*; + +@Component +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(); + } + } +} diff --git a/core/src/mindustry/entities/def/TimedComp.java b/core/src/mindustry/entities/def/TimedComp.java new file mode 100644 index 0000000000..3e33dd7622 --- /dev/null +++ b/core/src/mindustry/entities/def/TimedComp.java @@ -0,0 +1,25 @@ +package mindustry.entities.def; + +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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; + } +} diff --git a/core/src/mindustry/entities/def/TimerComp.java b/core/src/mindustry/entities/def/TimerComp.java new file mode 100644 index 0000000000..a23e8e09ac --- /dev/null +++ b/core/src/mindustry/entities/def/TimerComp.java @@ -0,0 +1,13 @@ +package mindustry.entities.def; + +import arc.util.*; +import mindustry.annotations.Annotations.*; + +@Component +abstract class TimerComp{ + Interval timer = new Interval(6); + + public boolean timer(int index, float time){ + return timer.get(index, time); + } +} diff --git a/core/src/mindustry/entities/def/UnitComp.java b/core/src/mindustry/entities/def/UnitComp.java new file mode 100644 index 0000000000..dbaa5cf9be --- /dev/null +++ b/core/src/mindustry/entities/def/UnitComp.java @@ -0,0 +1,127 @@ +package mindustry.entities.def; + +import arc.*; +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.content.*; +import mindustry.entities.*; +import mindustry.entities.units.*; +import mindustry.game.EventType.*; +import mindustry.gen.*; +import mindustry.graphics.*; +import mindustry.type.*; +import mindustry.world.*; +import mindustry.world.blocks.*; + +import static mindustry.Vars.*; + +@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; + controller(type.createController()); + 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); + //} + } +} diff --git a/core/src/mindustry/entities/def/VelComp.java b/core/src/mindustry/entities/def/VelComp.java new file mode 100644 index 0000000000..d47e5f1ba4 --- /dev/null +++ b/core/src/mindustry/entities/def/VelComp.java @@ -0,0 +1,22 @@ +package mindustry.entities.def; + +import arc.math.geom.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.gen.*; + +@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()); + } +} diff --git a/core/src/mindustry/entities/def/WeaponsComp.java b/core/src/mindustry/entities/def/WeaponsComp.java new file mode 100644 index 0000000000..cec56eaade --- /dev/null +++ b/core/src/mindustry/entities/def/WeaponsComp.java @@ -0,0 +1,148 @@ +package mindustry.entities.def; + +import arc.graphics.g2d.*; +import arc.math.*; +import arc.util.*; +import mindustry.annotations.Annotations.*; +import mindustry.entities.*; +import mindustry.entities.bullet.*; +import mindustry.entities.units.*; +import mindustry.gen.*; +import mindustry.type.*; + +@Component +abstract 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)); + } +} diff --git a/core/src/mindustry/graphics/RenderLayer.java b/core/src/mindustry/graphics/RenderLayer.java index 69d58af407..baa43e5794 100644 --- a/core/src/mindustry/graphics/RenderLayer.java +++ b/core/src/mindustry/graphics/RenderLayer.java @@ -7,5 +7,6 @@ public enum RenderLayer{ flyingShadows, flying, bullets, - effects + effects, + names, } diff --git a/gradle.properties b/gradle.properties index 2d1a5796da..691ab50149 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.daemon=true org.gradle.jvmargs=-Xms256m -Xmx1024m -archash=6c4b3d49c647e1d594bfd9c3e40db922f8516e5d +archash=a57e709113a364ff718821de4c40366e17353329