diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index b3698223b3..faf8bfe159 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -818,6 +818,7 @@ ability.shieldregenfield = Shield Regen Field ability.movelightning = Movement Lightning ability.energyfield = Energy Field: [accent]{0}[] damage ~ [accent]{1}[] blocks / [accent]{2}[] targets +bar.onlycoredeposit = Only Core Depositing Allowed bar.drilltierreq = Better Drill Required bar.noresources = Missing Resources bar.corereq = Core Base Required diff --git a/core/assets/icons/icons.properties b/core/assets/icons/icons.properties index cc79778b44..362ff0fca5 100755 --- a/core/assets/icons/icons.properties +++ b/core/assets/icons/icons.properties @@ -545,3 +545,4 @@ 63147=blast-door|block-blast-door-ui 63146=alphaaaa|alphaaaa 63145=malis|team-malis +63144=canvas|block-canvas-ui diff --git a/core/assets/logicids.dat b/core/assets/logicids.dat index 4567c7e3d9..17c2dd1ee2 100644 Binary files a/core/assets/logicids.dat and b/core/assets/logicids.dat differ diff --git a/core/src/mindustry/content/Blocks.java b/core/src/mindustry/content/Blocks.java index da60c277ca..74355d6752 100644 --- a/core/src/mindustry/content/Blocks.java +++ b/core/src/mindustry/content/Blocks.java @@ -152,6 +152,7 @@ public class Blocks{ //logic message, switchBlock, microProcessor, logicProcessor, hyperProcessor, largeLogicDisplay, logicDisplay, memoryCell, memoryBank, + canvas, worldProcessor, worldCell, //campaign @@ -1588,7 +1589,6 @@ public class Blocks{ requirements(Category.defense, with(Items.beryllium, 6)); health = 130 * wallHealthMultiplier; armor = 2f; - buildCostMultiplier = 4f; }}; berylliumWallLarge = new Wall("beryllium-wall-large"){{ @@ -1596,14 +1596,12 @@ public class Blocks{ health = 130 * wallHealthMultiplier * 4; armor = 2f; size = 2; - buildCostMultiplier = 4f; }}; tungstenWall = new Wall("tungsten-wall"){{ requirements(Category.defense, with(Items.tungsten, 6)); health = 180 * wallHealthMultiplier; armor = 14f; - buildCostMultiplier = 4f; }}; tungstenWallLarge = new Wall("tungsten-wall-large"){{ @@ -1611,7 +1609,6 @@ public class Blocks{ health = 180 * wallHealthMultiplier * 4; armor = 14f; size = 2; - buildCostMultiplier = 4f; }}; blastDoor = new AutoDoor("blast-door"){{ @@ -1619,14 +1616,12 @@ public class Blocks{ health = 175 * wallHealthMultiplier * 4; armor = 14f; size = 2; - buildCostMultiplier = 4f; }}; carbideWall = new Wall("carbide-wall"){{ requirements(Category.defense, with(Items.thorium, 6, Items.carbide, 6)); health = 240 * wallHealthMultiplier; armor = 16f; - buildCostMultiplier = 4f; }}; carbideWallLarge = new Wall("carbide-wall-large"){{ @@ -1634,7 +1629,6 @@ public class Blocks{ health = 240 * wallHealthMultiplier * 4; armor = 16f; size = 2; - buildCostMultiplier = 4f; }}; mender = new MendProjector("mender"){{ @@ -2987,7 +2981,7 @@ public class Blocks{ //TODO these may work in space, but what's the point? lancer = new PowerTurret("lancer"){{ - requirements(Category.turret, with(Items.copper, 70, Items.lead, 70, Items.silicon, 70)); + requirements(Category.turret, with(Items.copper, 60, Items.lead, 70, Items.silicon, 60, Items.titanium, 30)); range = 165f; shoot.firstShotDelay = 40f; @@ -3003,6 +2997,7 @@ public class Blocks{ scaledHealth = 280; targetAir = false; moveWhileCharging = false; + accurateDelay = false; shootSound = Sounds.laser; coolant = consume(new ConsumeCoolant(0.2f)); @@ -3683,7 +3678,7 @@ public class Blocks{ coolantMultiplier = 6f; shootShake = 1f; - ammoPerShot = 4; + ammoPerShot = 2; drawer = new DrawTurret("reinforced-"); shootY = -2; outlineColor = Pal.darkOutline; @@ -4484,6 +4479,15 @@ public class Blocks{ size = 6; }}; + canvas = new CanvasBlock("canvas"){{ + requirements(Category.logic, with(Items.silicon, 40)); + + canvasSize = 12; + padding = 7f / 4f * 2f; + + size = 2; + }}; + worldProcessor = new LogicBlock("world-processor"){{ requirements(Category.logic, BuildVisibility.editorOnly, with()); diff --git a/core/src/mindustry/content/ErekirTechTree.java b/core/src/mindustry/content/ErekirTechTree.java index e14124b06e..2d05ba32a0 100644 --- a/core/src/mindustry/content/ErekirTechTree.java +++ b/core/src/mindustry/content/ErekirTechTree.java @@ -225,7 +225,7 @@ public class ErekirTechTree{ }); }); - node(radar, Seq.with(new Research(beamNode), new Research(turbineCondenser), new Research(fabricator)), () -> { + node(radar, Seq.with(new Research(beamNode), new Research(turbineCondenser), new Research(fabricator), new OnSector(SectorPresets.two)), () -> { node(breach, Seq.with(new Research(siliconArcFurnace), new OnSector(two)), () -> { node(berylliumWall, () -> { node(berylliumWallLarge, () -> { diff --git a/core/src/mindustry/content/Planets.java b/core/src/mindustry/content/Planets.java index 60073141f9..99623ffd69 100644 --- a/core/src/mindustry/content/Planets.java +++ b/core/src/mindustry/content/Planets.java @@ -74,6 +74,7 @@ public class Planets{ r.fog = true; r.staticFog = true; //TODO decide, is this a good idea? r.lighting = false; + r.onlyDepositCore = true; }; unlockedOnLand.add(Blocks.coreBastion); diff --git a/core/src/mindustry/content/UnitTypes.java b/core/src/mindustry/content/UnitTypes.java index d78bda9e8f..e61dd98a21 100644 --- a/core/src/mindustry/content/UnitTypes.java +++ b/core/src/mindustry/content/UnitTypes.java @@ -3450,6 +3450,7 @@ public class UnitTypes{ fogRadius = 0f; targetable = false; + hittable = false; setEnginesMirror( new UnitEngine(21 / 4f, 19 / 4f, 2.2f, 45f), @@ -3509,6 +3510,7 @@ public class UnitTypes{ fogRadius = 0f; targetable = false; + hittable = false; engineOffset = 7.2f; engineSize = 3.1f; @@ -3583,6 +3585,7 @@ public class UnitTypes{ fogRadius = 0f; targetable = false; + hittable = false; engineOffset = 7.5f; engineSize = 3.4f; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 3dc0c5e05b..f4a2780551 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -160,7 +160,7 @@ public class Logic implements ApplicationListener{ //makes cores go derelict in RTS mode, helps clean things up if(e.tile.build instanceof CoreBuild core && core.team.isAI() && core.team.rules().rtsAi){ Core.app.post(() -> { - core.team.data().makeDerelict(core.x, core.y, state.rules.enemyCoreBuildRadius); + core.team.data().timeDestroy(core.x, core.y, state.rules.enemyCoreBuildRadius); }); } }); diff --git a/core/src/mindustry/entities/Damage.java b/core/src/mindustry/entities/Damage.java index 7683a44043..7ee218222f 100644 --- a/core/src/mindustry/entities/Damage.java +++ b/core/src/mindustry/entities/Damage.java @@ -171,7 +171,7 @@ public class Damage{ }); Units.nearbyEnemies(b.team, rect, u -> { - if(u.checkTarget(b.type.collidesAir, b.type.collidesGround)){ + if(u.checkTarget(b.type.collidesAir, b.type.collidesGround) && u.type.hittable){ distances.add(u.dst(b)); } }); @@ -272,6 +272,7 @@ public class Damage{ float x2 = vec.x + x, y2 = vec.y + y; Cons cons = e -> { + if(!e.type.hittable) return; //the peirce cap works for units, but really terribly, I'm just disabling it for now. //if(pierceCap > 0 && pierceCount > pierceCap) return; @@ -373,7 +374,7 @@ public class Damage{ /** Damages all entities and blocks in a radius that are enemies of the team. */ public static void damageUnits(Team team, float x, float y, float size, float damage, Boolf predicate, Cons acceptor){ Cons cons = entity -> { - if(!predicate.get(entity)) return; + if(!predicate.get(entity) || !entity.type.hittable) return; entity.hitbox(hitrect); if(!hitrect.overlaps(rect)){ @@ -437,7 +438,7 @@ public class Damage{ /** Damages all entities and blocks in a radius that are enemies of the team. */ public static void damage(Team team, float x, float y, float radius, float damage, boolean complete, boolean air, boolean ground, boolean scaled, Bullet source){ Cons cons = entity -> { - if(entity.team == team || !entity.within(x, y, radius + (scaled ? entity.hitSize / 2f : 0f)) || (entity.isFlying() && !air) || (entity.isGrounded() && !ground)){ + if(entity.team == team || !entity.type.hittable || !entity.within(x, y, radius + (scaled ? entity.hitSize / 2f : 0f)) || (entity.isFlying() && !air) || (entity.isGrounded() && !ground)){ return; } diff --git a/core/src/mindustry/entities/EntityCollisions.java b/core/src/mindustry/entities/EntityCollisions.java index 233f3760e5..2bf034b95b 100644 --- a/core/src/mindustry/entities/EntityCollisions.java +++ b/core/src/mindustry/entities/EntityCollisions.java @@ -159,7 +159,7 @@ public class EntityCollisions{ float vbx = b.getX() - b.lastX(); float vby = b.getY() - b.lastY(); - if(a != b && a.collides(b)){ + if(a != b && a.collides(b) && b.collides(a)){ l1.set(a.getX(), a.getY()); boolean collide = r1.overlaps(r2) || collide(r1.x, r1.y, r1.width, r1.height, vax, vay, r2.x, r2.y, r2.width, r2.height, vbx, vby, l1); diff --git a/core/src/mindustry/entities/comp/UnitComp.java b/core/src/mindustry/entities/comp/UnitComp.java index 582d3df4ff..b295ff0d1e 100644 --- a/core/src/mindustry/entities/comp/UnitComp.java +++ b/core/src/mindustry/entities/comp/UnitComp.java @@ -257,6 +257,12 @@ abstract class UnitComp implements Healthc, Physicsc, Hitboxc, Statusc, Teamc, I return type.isCounted; } + @Override + @Replace + public boolean collides(Hitboxc other){ + return type.hittable; + } + @Override public int itemCapacity(){ return type.itemCapacity; diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 0109f38c96..21c485f9f0 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -79,6 +79,8 @@ public class Rules{ public boolean placeRangeCheck = false; /** If true, dead teams in PvP automatically have their blocks & units converted to derelict upon death. */ public boolean cleanupDeadTeams = true; + /** If true, items can only be deposited in the core. */ + public boolean onlyDepositCore = false; /** Radius around enemy wave drop zones.*/ public float dropZoneRadius = 300f; /** Time between waves in ticks. */ diff --git a/core/src/mindustry/game/Teams.java b/core/src/mindustry/game/Teams.java index d5b69c4f2d..9381746faf 100644 --- a/core/src/mindustry/game/Teams.java +++ b/core/src/mindustry/game/Teams.java @@ -305,6 +305,20 @@ public class Teams{ } } + /** Make all buildings within this range explode. */ + public void timeDestroy(float x, float y, float range){ + var builds = new Seq(); + if(buildings != null){ + buildings.intersect(x - range, y - range, range * 2f, range * 2f, builds); + } + + for(var build : builds){ + if(build.within(x, y, range)){ + Time.run(Mathf.random(0f, 60f * 6f), build::kill); + } + } + } + private void scheduleDerelict(Building build){ //TODO this may cause a lot of packet spam, optimize? Call.setTeam(build, Team.derelict); diff --git a/core/src/mindustry/graphics/OverlayRenderer.java b/core/src/mindustry/graphics/OverlayRenderer.java index 1a778fdf3f..bd2c16c503 100644 --- a/core/src/mindustry/graphics/OverlayRenderer.java +++ b/core/src/mindustry/graphics/OverlayRenderer.java @@ -238,14 +238,19 @@ public class OverlayRenderer{ Lines.circle(v.x, v.y, 6 + Mathf.absin(Time.time, 5f, 1f)); Draw.reset(); - Building tile = world.buildWorld(v.x, v.y); - if(input.canDropItem() && tile != null && tile.interactable(player.team()) && tile.acceptStack(player.unit().item(), player.unit().stack.amount, player.unit()) > 0 && player.within(tile, itemTransferRange)){ + Building build = world.buildWorld(v.x, v.y); + if(input.canDropItem() && build != null && build.interactable(player.team()) && build.acceptStack(player.unit().item(), player.unit().stack.amount, player.unit()) > 0 && player.within(build, itemTransferRange)){ + boolean invalid = (state.rules.onlyDepositCore && !(build instanceof CoreBuild)); + Lines.stroke(3f, Pal.gray); - Lines.square(tile.x, tile.y, tile.block.size * tilesize / 2f + 3 + Mathf.absin(Time.time, 5f, 1f)); - Lines.stroke(1f, Pal.place); - Lines.square(tile.x, tile.y, tile.block.size * tilesize / 2f + 2 + Mathf.absin(Time.time, 5f, 1f)); + Lines.square(build.x, build.y, build.block.size * tilesize / 2f + 3 + Mathf.absin(Time.time, 5f, 1f)); + Lines.stroke(1f, invalid ? Pal.remove : Pal.place); + Lines.square(build.x, build.y, build.block.size * tilesize / 2f + 2 + Mathf.absin(Time.time, 5f, 1f)); Draw.reset(); + if(invalid){ + build.block.drawPlaceText(Core.bundle.get("bar.onlycoredeposit"), build.tileX(), build.tileY(), false); + } } } } diff --git a/core/src/mindustry/input/InputHandler.java b/core/src/mindustry/input/InputHandler.java index 6cc5bc95d5..fc31e89149 100644 --- a/core/src/mindustry/input/InputHandler.java +++ b/core/src/mindustry/input/InputHandler.java @@ -36,6 +36,7 @@ import mindustry.world.blocks.*; import mindustry.world.blocks.distribution.*; import mindustry.world.blocks.payloads.*; import mindustry.world.blocks.storage.*; +import mindustry.world.blocks.storage.CoreBlock.*; import mindustry.world.meta.*; import java.util.*; @@ -270,7 +271,7 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ @Remote(targets = Loc.both, forward = true, called = Loc.server) public static void transferInventory(Player player, Building build){ - if(player == null || build == null || !player.within(build, itemTransferRange) || build.items == null || player.dead()) return; + if(player == null || build == null || !player.within(build, itemTransferRange) || build.items == null || player.dead() || (state.rules.onlyDepositCore && !(build instanceof CoreBuild))) return; if(net.server() && (player.unit().stack.amount <= 0 || !Units.canInteract(player, build) || !netServer.admins.allowAction(player, ActionType.depositItem, build.tile, action -> { @@ -1390,8 +1391,11 @@ public abstract class InputHandler implements InputProcessor, GestureListener{ ItemStack stack = player.unit().stack; - if(build != null && build.acceptStack(stack.item, stack.amount, player.unit()) > 0 && build.interactable(player.team()) && build.block.hasItems && player.unit().stack().amount > 0 && build.interactable(player.team())){ - Call.transferInventory(player, build); + if(build != null && build.acceptStack(stack.item, stack.amount, player.unit()) > 0 && build.interactable(player.team()) && + build.block.hasItems && player.unit().stack().amount > 0 && build.interactable(player.team())){ + if(!(state.rules.onlyDepositCore && !(build instanceof CoreBuild))){ + Call.transferInventory(player, build); + } }else{ Call.dropItem(player.angleTo(x, y)); } diff --git a/core/src/mindustry/type/UnitType.java b/core/src/mindustry/type/UnitType.java index 9123a48bcd..dd1de4118a 100644 --- a/core/src/mindustry/type/UnitType.java +++ b/core/src/mindustry/type/UnitType.java @@ -85,6 +85,8 @@ public class UnitType extends UnlockableContent{ public float targetPriority = 0f; /** If false, this unit is not targeted by anything. */ public boolean targetable = true; + /** If true, this unit can be hit with bullets/splash damage. */ + public boolean hittable = true; public boolean drawBuildBeam = true; public boolean rotateToBuilding = true; public float commandRadius = 150f; diff --git a/core/src/mindustry/world/blocks/logic/CanvasBlock.java b/core/src/mindustry/world/blocks/logic/CanvasBlock.java new file mode 100644 index 0000000000..ea195291fe --- /dev/null +++ b/core/src/mindustry/world/blocks/logic/CanvasBlock.java @@ -0,0 +1,250 @@ +package mindustry.world.blocks.logic; + +import arc.graphics.*; +import arc.graphics.g2d.*; +import arc.input.*; +import arc.math.*; +import arc.math.geom.*; +import arc.scene.*; +import arc.scene.event.*; +import arc.scene.ui.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.gen.*; +import mindustry.ui.*; +import mindustry.world.*; + +import static mindustry.Vars.*; + +public class CanvasBlock extends Block{ + public float padding = 0f; + public int canvasSize = 8; + public int[] palette = {0x000000ff, 0x55415f_ff, 0x646964_ff, 0xd77355_ff, 0x508cd7_ff, 0x64b964_ff, 0xe6c86e_ff, 0xdcf5ff_ff}; + public int bitsPerPixel; + public IntIntMap colorToIndex = new IntIntMap(); + + public CanvasBlock(String name){ + super(name); + + configurable = true; + destructible = true; + solid = true; + + config(byte[].class, (CanvasBuild build, byte[] bytes) -> { + if(build.data.length == bytes.length){ + build.data = bytes; + build.updateTexture(); + } + }); + } + + @Override + public void init(){ + super.init(); + + for(int i = 0; i < palette.length; i++){ + colorToIndex.put(palette[i], i); + } + bitsPerPixel = Mathf.log2(Mathf.nextPowerOfTwo(palette.length)); + } + + public class CanvasBuild extends Building{ + public @Nullable Texture texture; + public byte[] data = new byte[Mathf.ceil(canvasSize * canvasSize * bitsPerPixel / 8f)]; + + public void updateTexture(){ + Pixmap pix = makePixmap(); + if(texture != null){ + texture.draw(pix); + }else{ + texture = new Texture(pix); + } + pix.dispose(); + } + + public Pixmap makePixmap(){ + Pixmap pix = new Pixmap(canvasSize, canvasSize); + int bpp = bitsPerPixel; + int pixels = canvasSize * canvasSize; + for(int i = 0; i < pixels; i++){ + int bitOffset = i * bpp; + int pal = getByte(bitOffset); + pix.set(i % canvasSize, i / canvasSize, palette[pal]); + } + return pix; + } + + public byte[] packPixmap(Pixmap pixmap){ + byte[] bytes = new byte[data.length]; + int pixels = canvasSize * canvasSize; + for(int i = 0; i < pixels; i++){ + int color = pixmap.get(i % canvasSize, i / canvasSize); + int palIndex = colorToIndex.get(color); + setByte(bytes, i * bitsPerPixel, palIndex); + } + return bytes; + } + + protected int getByte(int bitOffset){ + int result = 0, bpp = bitsPerPixel; + for(int i = 0; i < bpp; i++){ + int word = i + bitOffset >>> 3; + result |= (((data[word] & (1 << (i + bitOffset & 7))) == 0 ? 0 : 1) << i); + } + return result; + } + + protected void setByte(byte[] bytes, int bitOffset, int value){ + int bpp = bitsPerPixel; + for(int i = 0; i < bpp; i++){ + int word = i + bitOffset >>> 3; + + if(((value >>> i) & 1) == 0){ + bytes[word] &= ~(1 << (i + bitOffset & 7)); + }else{ + bytes[word] |= (1 << (i + bitOffset & 7)); + } + } + } + + @Override + public void draw(){ + super.draw(); + + if(texture == null){ + updateTexture(); + } + Tmp.tr1.set(texture); + Draw.rect(Tmp.tr1, x, y, size * tilesize - padding, size * tilesize - padding); + } + + @Override + public void remove(){ + super.remove(); + if(texture != null){ + texture.dispose(); + texture = null; + } + } + + @Override + public void buildConfiguration(Table table){ + table.button(Icon.pencil, Styles.clearTransi, () -> { + Dialog dialog = new Dialog(); + + Pixmap pix = makePixmap(); + Texture texture = new Texture(pix); + int[] curColor = {palette[0]}; + boolean[] modified = {false}; + + dialog.cont.table(Tex.pane, body -> { + body.stack(new Element(){ + int lastX, lastY; + + { + addListener(new InputListener(){ + int convertX(float ex){ + return (int)((ex - x) / width * canvasSize); + } + + int convertY(float ey){ + return pix.height - 1 - (int)((ey - y) / height * canvasSize); + } + + @Override + public boolean touchDown(InputEvent event, float ex, float ey, int pointer, KeyCode button){ + int cx = convertX(ex), cy = convertY(ey); + draw(cx, cy); + lastX = cx; + lastY = cy; + return true; + } + + @Override + public void touchDragged(InputEvent event, float ex, float ey, int pointer){ + int cx = convertX(ex), cy = convertY(ey); + Bresenham2.line(lastX, lastY, cx, cy, (x, y) -> draw(x, y)); + lastX = cx; + lastY = cy; + } + }); + } + + void draw(int x, int y){ + if(pix.get(x, y) != curColor[0]){ + pix.set(x, y, curColor[0]); + Pixmaps.drawPixel(texture, x, y, curColor[0]); + modified[0] = true; + } + } + + @Override + public void draw(){ + Tmp.tr1.set(texture); + Draw.alpha(parentAlpha); + Draw.rect(Tmp.tr1, x + width/2f, y + height/2f, width, height); + } + }, new GridImage(canvasSize, canvasSize){{ + touchable = Touchable.disabled; + }}).size(500f); + }); + + dialog.cont.row(); + + dialog.cont.table(Tex.button, p -> { + for(int i = 0; i < palette.length; i++){ + int fi = i; + + var button = p.button(Tex.whiteui, Styles.clearTogglei, 30, () -> { + curColor[0] = palette[fi]; + }).size(44).checked(b -> curColor[0] == palette[fi]).get(); + button.getStyle().imageUpColor = new Color(palette[i]); + } + }); + + dialog.closeOnBack(); + + dialog.buttons.defaults().size(150f, 64f); + dialog.buttons.button("@cancel", Icon.cancel, dialog::hide); + dialog.buttons.button("@ok", Icon.ok, () -> { + if(modified[0]){ + configure(packPixmap(pix)); + pix.dispose(); + texture.dispose(); + } + dialog.hide(); + }); + + dialog.show(); + }).size(40f); + } + + @Override + public byte[] config(){ + return data; + } + + @Override + public void write(Writes write){ + super.write(write); + + //for future canvas resizing events + write.i(data.length); + write.b(data); + } + + @Override + public void read(Reads read, byte revision){ + super.read(read, revision); + + int len = read.i(); + if(data.length == len){ + read.b(data); + }else{ + read.skip(len); + } + } + } +} diff --git a/core/src/mindustry/world/blocks/logic/MessageBlock.java b/core/src/mindustry/world/blocks/logic/MessageBlock.java index 1c373056dc..6abae67046 100644 --- a/core/src/mindustry/world/blocks/logic/MessageBlock.java +++ b/core/src/mindustry/world/blocks/logic/MessageBlock.java @@ -87,7 +87,7 @@ public class MessageBlock extends Block{ @Override public void buildConfiguration(Table table){ - table.button(Icon.pencil, () -> { + table.button(Icon.pencil, Styles.clearTransi, () -> { if(mobile){ Core.input.getTextInput(new TextInput(){{ text = message.toString();