package mindustry.world; import arc.*; import arc.audio.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.graphics.g2d.TextureAtlas.*; import arc.math.*; import arc.math.geom.*; import arc.scene.ui.layout.*; import arc.struct.EnumSet; import arc.struct.*; import arc.util.*; import arc.util.pooling.*; 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.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.graphics.MultiPacker.*; import mindustry.logic.*; import mindustry.type.*; import mindustry.ui.*; import mindustry.world.blocks.*; import mindustry.world.blocks.environment.*; import mindustry.world.blocks.power.*; import mindustry.world.consumers.*; import mindustry.world.meta.*; import java.lang.reflect.*; import java.util.*; import static mindustry.Vars.*; public class Block extends UnlockableContent implements Senseable{ /** If true, buildings have an ItemModule. */ public boolean hasItems; /** If true, buildings have a LiquidModule. */ public boolean hasLiquids; /** If true, buildings have a PowerModule. */ public boolean hasPower; /** Flag for determining whether this block outputs liquid somewhere; used for connections. */ public boolean outputsLiquid = false; /** Used by certain power blocks (nodes) to flag as non-consuming of power. True by default, even if this block has no power. */ public boolean consumesPower = true; /** If true, this block is a generator that can produce power. */ public boolean outputsPower = false; /** If false, power nodes cannot connect to this block. */ public boolean connectedPower = true; /** If true, this block can conduct power like a cable. */ public boolean conductivePower = false; /** If true, this block can output payloads; affects blending. */ public boolean outputsPayload = false; /** If true, this block can input payloads; affects unit payload enter behavior. */ public boolean acceptsPayloads = false; /** If true, payloads will attempt to move into this block. */ public boolean acceptsPayload = false; /** Visual flag use for blending of certain transportation blocks. */ public boolean acceptsItems = false; /** If true, all item capacities of this block are separate instead of pooled as one number. */ public boolean separateItemCapacity = false; /** maximum items this block can carry (usually, this is per-type of item) */ public int itemCapacity = 10; /** maximum total liquids this block can carry if hasLiquids = true */ public float liquidCapacity = 10f; /** higher numbers increase liquid output speed; TODO remove and replace with better liquids system */ public float liquidPressure = 1f; /** If true, this block outputs to its facing direction, when applicable. * Used for blending calculations. */ public boolean outputFacing = true; /** if true, this block does not accept input from the sides (used for armored conveyors) */ public boolean noSideBlend = false; /** whether to display flow rate */ public boolean displayFlow = true; /** whether this block is visible in the editor */ public boolean inEditor = true; /** the last configuration value applied to this block. */ public @Nullable Object lastConfig; /** whether to save the last config and apply it to newly placed blocks */ public boolean saveConfig = false; /** whether to allow copying the config through middle click */ public boolean copyConfig = true; /** if true, double-tapping this configurable block clears configuration. */ public boolean clearOnDoubleTap = false; /** whether this block has a tile entity that updates */ public boolean update; /** whether this block has health and can be destroyed */ public boolean destructible; /** whether unloaders work on this block */ public boolean unloadable = true; /** if true, this block acts a duct and will connect to armored ducts from the side. */ public boolean isDuct = false; /** whether units can resupply by taking items from this block */ public boolean allowResupply = false; /** whether this is solid */ public boolean solid; /** whether this block CAN be solid. */ public boolean solidifes; /** if true, this counts as a non-solid block to this team. */ public boolean teamPassable; /** if true, this block cannot be hit by bullets unless explicitly targeted. */ public boolean underBullets; /** whether this is rotatable */ public boolean rotate; /** if rotate is true and this is false, the region won't rotate when drawing */ public boolean rotateDraw = true; /** if rotate = false and this is true, rotation will be locked at 0 when placing (default); advanced use only */ public boolean lockRotation = true; /** if true, schematic flips with this block are inverted. */ public boolean invertFlip = false; /** number of different variant regions to use */ public int variants = 0; /** whether to draw a rotation arrow - this does not apply to lines of blocks */ public boolean drawArrow = true; /** whether to draw the team corner by default */ public boolean drawTeamOverlay = true; /** for static blocks only: if true, tile data() is saved in world data. */ public boolean saveData; /** whether you can break this with rightclick */ public boolean breakable; /** whether to add this block to brokenblocks */ public boolean rebuildable = true; /** if true, this logic-related block can only be used with privileged processors (or is one itself) */ public boolean privileged = false; /** whether this block can only be placed on water */ public boolean requiresWater = false; /** whether this block can be placed on any liquids, anywhere */ public boolean placeableLiquid = false; /** whether this block can be placed directly by the player via PlacementFragment */ public boolean placeablePlayer = true; /** whether this floor can be placed on. */ public boolean placeableOn = true; /** whether this block has insulating properties. */ public boolean insulated = false; /** whether the sprite is a full square. */ public boolean squareSprite = true; /** whether this block absorbs laser attacks. */ public boolean absorbLasers = false; /** if false, the status is never drawn */ public boolean enableDrawStatus = true; /** whether to draw disabled status */ public boolean drawDisabled = true; /** whether to automatically reset enabled status after a logic block has not interacted for a while. */ public boolean autoResetEnabled = true; /** if true, the block stops updating when disabled */ public boolean noUpdateDisabled = false; /** if true, this block updates when it's a payload in a unit. */ public boolean updateInUnits = true; /** if true, this block updates in payloads in units regardless of the experimental game rule */ public boolean alwaysUpdateInUnits = false; /** if false, only incinerable liquids are dropped when deconstructing; otherwise, all liquids are dropped. */ public boolean deconstructDropAllLiquid = false; /** Whether to use this block's color in the minimap. Only used for overlays. */ public boolean useColor = true; /** item that drops from this block, used for drills */ public @Nullable Item itemDrop = null; /** if true, this block cannot be mined by players. useful for annoying things like sand. */ public boolean playerUnmineable = false; /** Array of affinities to certain things. */ public Attributes attributes = new Attributes(); /** Health per square tile that this block occupies; essentially, this is multiplied by size * size. Overridden if health is > 0. If <0, the default is 40. */ public float scaledHealth = -1; /** building health; -1 to use scaledHealth */ public int health = -1; /** damage absorption, similar to unit armor */ public float armor = 0f; /** base block explosiveness */ public float baseExplosiveness = 0f; /** bullet that this block spawns when destroyed */ public @Nullable BulletType destroyBullet = null; /** if true, destroyBullet is spawned on the block's team instead of Derelict team */ public boolean destroyBulletSameTeam = false; /** liquid used for lighting */ public @Nullable Liquid lightLiquid; /** whether cracks are drawn when this block is damaged */ public boolean drawCracks = true; /** whether rubble is created when this block is destroyed */ public boolean createRubble = true; /** whether this block can be placed on edges of liquids. */ public boolean floating = false; /** multiblock size */ public int size = 1; /** multiblock offset */ public float offset = 0f; /** offset for iteration (internal use only) */ public int sizeOffset = 0; /** Clipping size of this block. Should be as large as the block will draw. */ public float clipSize = -1f; /** When placeRangeCheck is enabled, this is the range checked for enemy blocks. */ public float placeOverlapRange = 50f; /** Multiplier of damage dealt to this block by tanks. Does not apply to crawlers. */ public float crushDamageMultiplier = 1f; /** Max of timers used. */ public int timers = 0; /** Cache layer. Only used for 'cached' rendering. */ public CacheLayer cacheLayer = CacheLayer.normal; /** Special flag; if false, floor will be drawn under this block even if it is cached. */ public boolean fillsTile = true; /** If true, this block can be covered by darkness / fog even if synthetic. */ public boolean forceDark = false; /** whether this block can be replaced in all cases */ public boolean alwaysReplace = false; /** if false, this block can never be replaced. */ public boolean replaceable = true; /** The block group. Unless {@link #canReplace} is overridden, blocks in the same group can replace each other. */ public BlockGroup group = BlockGroup.none; /** List of block flags. Used for AI indexing. */ public EnumSet flags = EnumSet.of(); /** Targeting priority of this block, as seen by enemies. */ public float priority = TargetPriority.base; /** How much this block affects the unit cap by. * The block flags must contain unitModifier in order for this to work. */ public int unitCapModifier = 0; /** Whether the block can be tapped and selected to configure. */ public boolean configurable; /** If true, this building can be selected like a unit when commanding. */ public boolean commandable; /** If true, the building inventory can be shown with the config. */ public boolean allowConfigInventory = true; /** Defines how large selection menus, such as that of sorters, should be. */ public int selectionRows = 5, selectionColumns = 4; /** If true, this block can be configured by logic. */ public boolean logicConfigurable = false; /** Whether this block consumes touchDown events when tapped. */ public boolean consumesTap; /** Whether to draw the glow of the liquid for this block, if it has one. */ public boolean drawLiquidLight = true; /** Environmental flags that are *all* required for this block to function. 0 = any environment */ public int envRequired = 0; /** The environment flags that this block can function in. If the env matches any of these, it will be enabled. */ public int envEnabled = Env.terrestrial; /** The environment flags that this block *cannot* function in. If the env matches any of these, it will be *disabled*. */ public int envDisabled = 0; /** Whether to periodically sync this block across the network. */ public boolean sync; /** Whether this block uses conveyor-type placement mode. */ public boolean conveyorPlacement; /** If false, diagonal placement (ctrl) for this block is not allowed. */ public boolean allowDiagonal = true; /** Whether to swap the diagonal placement modes. */ public boolean swapDiagonalPlacement; /** Build queue priority in schematics. */ public int schematicPriority = 0; /** * The color of this block when displayed on the minimap or map preview. * Do not set manually! This is overridden when loading for most blocks. */ public Color mapColor = new Color(0, 0, 0, 1); /** Whether this block has a minimap color. */ public boolean hasColor = false; /** Whether units target this block. */ public boolean targetable = true; /** If true, this block attacks and is considered a turret in the indexer. Building must implement Ranged. */ public boolean attacks = false; /** If true, this block is mending-related and can be suppressed with special units/missiles. */ public boolean suppressable = false; /** Whether the overdrive core has any effect on this block. */ public boolean canOverdrive = true; /** Outlined icon color.*/ public Color outlineColor = Color.valueOf("404049"); /** Whether any icon region has an outline added. */ public boolean outlineIcon = false; /** Outline icon radius. */ public int outlineRadius = 4; /** Which of the icon regions gets the outline added. Uses last icon if <= 0. */ public int outlinedIcon = -1; /** Whether this block has a shadow under it. */ public boolean hasShadow = true; /** If true, a custom shadow (name-shadow) is drawn under this block. */ public boolean customShadow = false; /** Should the sound made when this block is built change in pitch. */ public boolean placePitchChange = true; /** Should the sound made when this block is deconstructed change in pitch. */ public boolean breakPitchChange = true; /** Sound made when this block is built. */ public Sound placeSound = Sounds.place; /** Sound made when this block is deconstructed. */ public Sound breakSound = Sounds.breaks; /** Sounds made when this block is destroyed.*/ public Sound destroySound = Sounds.boom; /** How reflective this block is. */ public float albedo = 0f; /** Environmental passive light color. */ public Color lightColor = Color.white.cpy(); /** * Whether this environmental block passively emits light. * Does not change behavior for non-environmental blocks, but still updates clipSize. */ public boolean emitLight = false; /** Radius of the light emitted by this block. */ public float lightRadius = 60f; /** How much fog this block uncovers, in tiles. Cannot be dynamic. <= 0 to disable. */ public int fogRadius = -1; /** The sound that this block makes while active. One sound loop. Do not overuse. */ public Sound loopSound = Sounds.none; /** Active sound base volume. */ public float loopSoundVolume = 0.5f; /** The sound that this block makes while idle. Uses one sound loop for all blocks. */ public Sound ambientSound = Sounds.none; /** Idle sound base volume. */ public float ambientSoundVolume = 0.05f; /** Cost of constructing this block. */ public ItemStack[] requirements = {}; /** Category in place menu. */ public Category category = Category.distribution; /** Time to build this block in ticks; do not modify directly! */ public float buildCost = 20f; /** Whether this block is visible and can currently be built. */ public BuildVisibility buildVisibility = BuildVisibility.hidden; /** Multiplier for speed of building this block. */ public float buildCostMultiplier = 1f; /** Build completion at which deconstruction finishes. */ public float deconstructThreshold = 0f; /** If true, this block deconstructs immediately. Instant deconstruction implies no resource refund. */ public boolean instantDeconstruct = false; /** Effect for placing the block. Passes size as rotation. */ public Effect placeEffect = Fx.placeBlock; /** Effect for breaking the block. Passes size as rotation. */ public Effect breakEffect = Fx.breakBlock; /** Effect for destroying the block. */ public Effect destroyEffect = Fx.dynamicExplosion; /** Multiplier for cost of research in tech tree. */ public float researchCostMultiplier = 1; /** Cost multipliers per-item. */ public ObjectFloatMap researchCostMultipliers = new ObjectFloatMap<>(); /** Override for research cost. Uses multipliers above and building requirements if not set. */ public @Nullable ItemStack[] researchCost; /** Whether this block has instant transfer.*/ public boolean instantTransfer = false; /** Whether you can rotate this block after it is placed. */ public boolean quickRotate = true; /** If true, this derelict block can be repair by clicking it. */ public boolean allowDerelictRepair = true; /** Main subclass. Non-anonymous. */ public @Nullable Class subclass; /** Scroll position for certain blocks. */ public float selectScroll; /** Building that is created for this block. Initialized in init() via reflection. Set manually if modded. */ public Prov buildType = null; /** Configuration handlers by type. */ public ObjectMap, Cons2> configurations = new ObjectMap<>(); /** Consumption filters. */ public boolean[] itemFilter = {}, liquidFilter = {}; /** Array of consumers used by this block. Only populated after init(). */ public Consume[] consumers = {}, optionalConsumers = {}, nonOptionalConsumers = {}, updateConsumers = {}; /** Set to true if this block has any consumers in its array. */ public boolean hasConsumers; /** The single power consumer, if applicable. */ public @Nullable ConsumePower consPower; /** Map of bars by name. */ protected OrderedMap> barMap = new OrderedMap<>(); /** List for building up consumption before init(). */ protected Seq consumeBuilder = new Seq<>(); protected TextureRegion[] generatedIcons; protected TextureRegion[] editorVariantRegions; /** Regions indexes from icons() that are rotated. If either of these is not -1, other regions won't be rotated in ConstructBlocks. */ public int regionRotated1 = -1, regionRotated2 = -1; public TextureRegion region, editorIcon; public @Load("@-shadow") TextureRegion customShadowRegion; public @Load("@-team") TextureRegion teamRegion; public TextureRegion[] teamRegions, variantRegions, variantShadowRegions; protected static final Seq tempTiles = new Seq<>(); protected static final Seq tempBuilds = new Seq<>(); /** Dump timer ID.*/ protected final int timerDump = timers++; /** How often to try dumping items in ticks, e.g. 5 = 12 times/sec*/ protected final int dumpTime = 5; public Block(String name){ super(name); initBuilding(); selectionSize = 28f; } public void drawBase(Tile tile){ //delegates to entity unless it is null if(tile.build != null){ tile.build.draw(); }else{ Draw.rect( variants == 0 ? region : variantRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantRegions.length - 1))], tile.drawx(), tile.drawy()); } } public void drawShadow(Tile tile){ Draw.color(0f, 0f, 0f, BlockRenderer.shadowColor.a); Draw.rect( variants == 0 ? customShadowRegion : variantShadowRegions[Mathf.randomSeed(tile.pos(), 0, Math.max(0, variantShadowRegions.length - 1))], tile.drawx(), tile.drawy(), tile.build == null ? 0f : tile.build.drawrot()); Draw.color(); } public float percentSolid(int x, int y){ Tile tile = world.tile(x, y); if(tile == null) return 0; return tile.getLinkedTilesAs(this, tempTiles) .sumf(other -> !other.floor().isLiquid ? 1f : 0f) / size / size; } public void drawEnvironmentLight(Tile tile){ Drawf.light(tile.worldx(), tile.worldy(), lightRadius, lightColor, lightColor.a); } /** Drawn when you are placing a block. */ public void drawPlace(int x, int y, int rotation, boolean valid){ drawPotentialLinks(x, y); drawOverlay(x * tilesize + offset, y * tilesize + offset, rotation); } public void drawPotentialLinks(int x, int y){ if((consumesPower || outputsPower) && hasPower && connectedPower){ Tile tile = world.tile(x, y); if(tile != null){ PowerNode.getNodeLinks(tile, this, player.team(), other -> { PowerNode node = (PowerNode)other.block; Draw.color(node.laserColor1, Renderer.laserOpacity * 0.5f); node.drawLaser(x * tilesize + offset, y * tilesize + offset, other.x, other.y, size, other.block.size); Drawf.square(other.x, other.y, other.block.size * tilesize / 2f + 2f, Pal.place); }); } } } public float drawPlaceText(String text, int x, int y, boolean valid){ if(renderer.pixelator.enabled()) return 0; Color color = valid ? Pal.accent : Pal.remove; Font font = Fonts.outline; GlyphLayout layout = Pools.obtain(GlyphLayout.class, GlyphLayout::new); boolean ints = font.usesIntegerPositions(); font.setUseIntegerPositions(false); font.getData().setScale(1f / 4f / Scl.scl(1f)); layout.setText(font, text); float width = layout.width; font.setColor(color); float dx = x * tilesize + offset, dy = y * tilesize + offset + size * tilesize / 2f + 3; font.draw(text, dx, dy + layout.height + 1, Align.center); dy -= 1f; Lines.stroke(2f, Color.darkGray); Lines.line(dx - layout.width / 2f - 2f, dy, dx + layout.width / 2f + 1.5f, dy); Lines.stroke(1f, color); Lines.line(dx - layout.width / 2f - 2f, dy, dx + layout.width / 2f + 1.5f, dy); font.setUseIntegerPositions(ints); font.setColor(Color.white); font.getData().setScale(1f); Draw.reset(); Pools.free(layout); return width; } /** Drawn when placing and when hovering over. */ public void drawOverlay(float x, float y, int rotation){ } public float sumAttribute(@Nullable Attribute attr, int x, int y){ if(attr == null) return 0; Tile tile = world.tile(x, y); if(tile == null) return 0; return tile.getLinkedTilesAs(this, tempTiles) .sumf(other -> !floating && other.floor().isDeep() ? 0 : other.floor().attributes.get(attr)); } public TextureRegion getDisplayIcon(Tile tile){ return tile.build == null ? uiIcon : tile.build.getDisplayIcon(); } public String getDisplayName(Tile tile){ return tile.build == null ? localizedName : tile.build.getDisplayName(); } /** @return a custom minimap color for this or 0 to use default colors. */ public int minimapColor(Tile tile){ return 0; } public boolean outputsItems(){ return hasItems; } /** @return whether this block can be placed on the specified tile. */ public boolean canPlaceOn(Tile tile, Team team, int rotation){ return true; } /** @return whether this block can be broken on the specified tile. */ public boolean canBreak(Tile tile){ return true; } public boolean rotatedOutput(int x, int y){ return rotate; } public boolean synthetic(){ return update || destructible; } public boolean checkForceDark(Tile tile){ return forceDark; } @Override public void setStats(){ super.setStats(); stats.add(Stat.size, "@x@", size, size); if(synthetic()){ stats.add(Stat.health, health, StatUnit.none); if(armor > 0){ stats.add(Stat.armor, armor, StatUnit.none); } } if(canBeBuilt() && requirements.length > 0){ stats.add(Stat.buildTime, buildCost / 60, StatUnit.seconds); stats.add(Stat.buildCost, StatValues.items(false, requirements)); } if(instantTransfer){ stats.add(Stat.maxConsecutive, 2, StatUnit.none); } for(var c : consumers){ c.display(stats); } //Note: Power stats are added by the consumers. if(hasLiquids) stats.add(Stat.liquidCapacity, liquidCapacity, StatUnit.liquidUnits); if(hasItems && itemCapacity > 0) stats.add(Stat.itemCapacity, itemCapacity, StatUnit.items); } public void addBar(String name, Func sup){ barMap.put(name, (Func)sup); } public void removeBar(String name){ barMap.remove(name); } public Iterable> listBars(){ return barMap.values(); } public void addLiquidBar(Liquid liq){ addBar("liquid-" + liq.name, entity -> !liq.unlockedNow() ? null : new Bar( () -> liq.localizedName, liq::barColor, () -> entity.liquids.get(liq) / liquidCapacity )); } /** Adds a liquid bar that dynamically displays a liquid type. */ public void addLiquidBar(Func current){ addBar("liquid", entity -> new Bar( () -> current.get((T)entity) == null || entity.liquids.get(current.get((T)entity)) <= 0.001f ? Core.bundle.get("bar.liquid") : current.get((T)entity).localizedName, () -> current.get((T)entity) == null ? Color.clear : current.get((T)entity).barColor(), () -> current.get((T)entity) == null ? 0f : entity.liquids.get(current.get((T)entity)) / liquidCapacity) ); } public void setBars(){ addBar("health", entity -> new Bar("stat.health", Pal.health, entity::healthf).blink(Color.white)); if(consPower != null){ boolean buffered = consPower.buffered; float capacity = consPower.capacity; addBar("power", entity -> new Bar( () -> buffered ? Core.bundle.format("bar.poweramount", Float.isNaN(entity.power.status * capacity) ? "" : UI.formatAmount((int)(entity.power.status * capacity))) : Core.bundle.get("bar.power"), () -> Pal.powerBar, () -> Mathf.zero(consPower.requestedPower(entity)) && entity.power.graph.getPowerProduced() + entity.power.graph.getBatteryStored() > 0f ? 1f : entity.power.status) ); } if(hasItems && configurable){ addBar("items", entity -> new Bar( () -> Core.bundle.format("bar.items", entity.items.total()), () -> Pal.items, () -> (float)entity.items.total() / itemCapacity) ); } if(unitCapModifier != 0){ stats.add(Stat.maxUnits, (unitCapModifier < 0 ? "-" : "+") + Math.abs(unitCapModifier)); } //liquids added last if(hasLiquids){ //TODO liquids need to be handled VERY carefully. there are several potential possibilities: //1. no consumption or output (conduit/tank) // - display current(), 1 bar //2. static set of inputs and outputs // - create bars for each input/output, straightforward //3. TODO dynamic input/output combo??? // - confusion boolean added = false; //TODO handle in consumer //add bars for *specific* consumed liquids for(var consl : consumers){ if(consl instanceof ConsumeLiquid liq){ added = true; addLiquidBar(liq.liquid); }else if(consl instanceof ConsumeLiquids multi){ added = true; for(var stack : multi.liquids){ addLiquidBar(stack.liquid); } } } //nothing was added, so it's safe to add a dynamic liquid bar (probably?) if(!added){ addLiquidBar(build -> build.liquids.current()); } } } public boolean consumesItem(Item item){ return itemFilter[item.id]; } public boolean consumesLiquid(Liquid liq){ return liquidFilter[liq.id]; } public boolean canReplace(Block other){ if(other.alwaysReplace) return true; if(other.privileged) return false; return other.replaceable && (other != this || (rotate && quickRotate)) && ((this.group != BlockGroup.none && other.group == this.group) || other == this) && (size == other.size || (size >= other.size && ((subclass != null && subclass == other.subclass) || group.anyReplace))); } /** @return a possible replacement for this block when placed in a line by the player. */ public Block getReplacement(BuildPlan req, Seq plans){ return this; } /** Mutates the given list of points used during line placement. */ public void changePlacementPath(Seq points, int rotation, boolean diagonalOn){ changePlacementPath(points, rotation); } /** Mutates the given list of points used during line placement. */ public void changePlacementPath(Seq points, int rotation){ } /** Mutates the given list of plans used during line placement. */ public void handlePlacementLine(Seq plans){ } public boolean configSenseable(){ return configurations.containsKey(Item.class) || configurations.containsKey(Liquid.class) || configurations.containsKey(UnlockableContent.class) || configurations.containsKey(Block.class) || configurations.containsKey(UnitType.class); } public Object nextConfig(){ if(saveConfig && lastConfig != null){ return lastConfig; } return null; } /** Called when a new build plan is created in the player's queue. Blocks can maintain a reference to this plan and add configs to it later. */ public void onNewPlan(BuildPlan plan){ } public void drawPlan(BuildPlan plan, Eachable list, boolean valid){ drawPlan(plan, list, valid, 1f); } public void drawPlan(BuildPlan plan, Eachable list, boolean valid, float alpha){ Draw.reset(); Draw.mixcol(!valid ? Pal.breakInvalid : Color.white, (!valid ? 0.4f : 0.24f) + Mathf.absin(Time.globalTime, 6f, 0.28f)); Draw.alpha(alpha); float prevScale = Draw.scl; Draw.scl *= plan.animScale; drawPlanRegion(plan, list); Draw.scl = prevScale; Draw.reset(); } public void drawPlanRegion(BuildPlan plan, Eachable list){ drawDefaultPlanRegion(plan, list); } /** this is a different method so subclasses can call it even after overriding the base */ public void drawDefaultPlanRegion(BuildPlan plan, Eachable list){ TextureRegion reg = getPlanRegion(plan, list); Draw.rect(reg, plan.drawx(), plan.drawy(), !rotate || !rotateDraw ? 0 : plan.rotation * 90); if(plan.worldContext && player != null && teamRegion != null && teamRegion.found()){ if(teamRegions[player.team().id] == teamRegion) Draw.color(player.team().color); Draw.rect(teamRegions[player.team().id], plan.drawx(), plan.drawy()); Draw.color(); } drawPlanConfig(plan, list); } public TextureRegion getPlanRegion(BuildPlan plan, Eachable list){ return fullIcon; } public void drawPlanConfig(BuildPlan plan, Eachable list){ } public void drawPlanConfigCenter(BuildPlan plan, Object content, String region, boolean cross){ if(content == null){ if(cross){ Draw.rect("cross", plan.drawx(), plan.drawy()); } return; } Color color = content instanceof Item i ? i.color : content instanceof Liquid l ? l.color : null; if(color == null) return; Draw.color(color); Draw.rect(region, plan.drawx(), plan.drawy()); Draw.color(); } public void drawPlanConfigCenter(BuildPlan plan, Object content, String region){ drawPlanConfigCenter(plan, content, region, false); } public void drawPlanConfigTop(BuildPlan plan, Eachable list){ } /** Transforms the internal position of this config using the specified function, and return the result. */ public Object pointConfig(Object config, Cons transformer){ return config; } /** Configure when a null value is passed.*/ public void configClear(Cons cons){ configurations.put(void.class, (tile, value) -> cons.get((E)tile)); } /** Listen for a config by class type. */ public void config(Class type, Cons2 config){ configurations.put(type, config); } public boolean isAccessible(){ return (hasItems && itemCapacity > 0); } /** sets {@param out} to the index-th side outside of this block, using the given rotation. */ public void nearbySide(int x, int y, int rotation, int index, Point2 out){ int cornerX = x - (size-1)/2, cornerY = y - (size-1)/2, s = size; switch(rotation){ case 0 -> out.set(cornerX + s, cornerY + index); case 1 -> out.set(cornerX + index, cornerY + s); case 2 -> out.set(cornerX - 1, cornerY + index); case 3 -> out.set(cornerX + index, cornerY - 1); } } public Point2[] getEdges(){ return Edges.getEdges(size); } public Point2[] getInsideEdges(){ return Edges.getInsideEdges(size); } /** Iterate through ever grid position taken up by this block. */ public void iterateTaken(int x, int y, Intc2 placer){ if(isMultiblock()){ int offsetx = -(size - 1) / 2; int offsety = -(size - 1) / 2; for(int dx = 0; dx < size; dx++){ for(int dy = 0; dy < size; dy++){ placer.get(dx + offsetx + x, dy + offsety + y); } } }else{ placer.get(x, y); } } /** Never use outside of the editor! */ public TextureRegion editorIcon(){ return editorIcon == null ? (editorIcon = Core.atlas.find(name + "-icon-editor")) : editorIcon; } /** Never use outside of the editor! */ public TextureRegion[] editorVariantRegions(){ if(editorVariantRegions == null){ variantRegions(); editorVariantRegions = new TextureRegion[variantRegions.length]; for(int i = 0; i < variantRegions.length; i++){ AtlasRegion region = (AtlasRegion)variantRegions[i]; editorVariantRegions[i] = Core.atlas.find("editor-" + region.name); } } return editorVariantRegions; } /** @return special icons to outline and save with an -outline variant. Vanilla only. */ public TextureRegion[] makeIconRegions(){ return new TextureRegion[0]; } protected TextureRegion[] icons(){ //use team region in vanilla team blocks TextureRegion r = variants > 0 ? Core.atlas.find(name + "1") : region; return teamRegion.found() && minfo.mod == null ? new TextureRegion[]{r, teamRegions[Team.sharded.id]} : new TextureRegion[]{r}; } public void getRegionsToOutline(Seq out){ } public TextureRegion[] getGeneratedIcons(){ return generatedIcons == null ? (generatedIcons = icons()) : generatedIcons; } public void resetGeneratedIcons(){ generatedIcons = null; } public TextureRegion[] variantRegions(){ return variantRegions == null ? (variantRegions = new TextureRegion[]{fullIcon}) : variantRegions; } public boolean hasBuilding(){ return destructible || update; } public final Building newBuilding(){ return buildType.get(); } public void updateClipRadius(float size){ clipSize = Math.max(clipSize, size * tilesize + size * 2f); } public Rect bounds(int x, int y, Rect rect){ return rect.setSize(size * tilesize).setCenter(x * tilesize + offset, y * tilesize + offset); } public boolean isMultiblock(){ return size > 1; } public boolean isVisible(){ return !isHidden() && (state.rules.editor || (!state.rules.hideBannedBlocks || !state.rules.isBanned(this))); } public boolean isVisibleOn(Planet planet){ return !Structs.contains(requirements, i -> planet.hiddenItems.contains(i.item)); } public boolean isPlaceable(){ return isVisible() && (!state.rules.isBanned(this) || state.rules.editor) && supportsEnv(state.rules.env); } /** @return whether this block supports a specific environment. */ public boolean supportsEnv(int env){ return (envEnabled & env) != 0 && (envDisabled & env) == 0 && (envRequired == 0 || (envRequired & env) == envRequired); } /** Called when building of this block begins. */ public void placeBegan(Tile tile, Block previous){ } /** Called when building of this block begins. */ public void placeBegan(Tile tile, Block previous, @Nullable Unit builder){ placeBegan(tile, previous); } /** Called right before building of this block begins. */ public void beforePlaceBegan(Tile tile, Block previous){ } public boolean isFloor(){ return this instanceof Floor; } public boolean isOverlay(){ return this instanceof OverlayFloor; } public Floor asFloor(){ return (Floor)this; } public boolean isAir(){ return id == 0; } public boolean canBeBuilt(){ return buildVisibility != BuildVisibility.hidden && buildVisibility != BuildVisibility.debugOnly; } public boolean environmentBuildable(){ return (state.rules.hiddenBuildItems.isEmpty() || !Structs.contains(requirements, i -> state.rules.hiddenBuildItems.contains(i.item))); } public boolean isStatic(){ return cacheLayer == CacheLayer.walls; } public T findConsumer(Boolf filter){ return consumers.length == 0 ? (T)consumeBuilder.find(filter) : (T)Structs.find(consumers, filter); } public boolean hasConsumer(Consume cons){ return consumeBuilder.contains(cons); } public void removeConsumer(Consume cons){ if(consumers.length > 0){ throw new IllegalStateException("You can only remove consumers before init(). After init(), all consumers have already been initialized."); } consumeBuilder.remove(cons); } public ConsumeLiquid consumeLiquid(Liquid liquid, float amount){ return consume(new ConsumeLiquid(liquid, amount)); } public ConsumeLiquids consumeLiquids(LiquidStack... stacks){ return consume(new ConsumeLiquids(stacks)); } /** * Creates a consumer which directly uses power without buffering it. * @param powerPerTick The amount of power which is required each tick for 100% efficiency. * @return the created consumer object. */ public ConsumePower consumePower(float powerPerTick){ return consume(new ConsumePower(powerPerTick, 0.0f, false)); } /** Creates a consumer which only consumes power when the condition is met. */ public ConsumePower consumePowerCond(float usage, Boolf cons){ return consume(new ConsumePowerCondition(usage, (Boolf)cons)); } /** Creates a consumer that consumes a dynamic amount of power. */ public ConsumePower consumePowerDynamic(Floatf usage){ return consume(new ConsumePowerDynamic((Floatf)usage)); } /** * Creates a consumer which stores power. * @param powerCapacity The maximum capacity in power units. */ public ConsumePower consumePowerBuffered(float powerCapacity){ return consume(new ConsumePower(0f, powerCapacity, true)); } public ConsumeItems consumeItem(Item item){ return consumeItem(item, 1); } public ConsumeItems consumeItem(Item item, int amount){ return consume(new ConsumeItems(new ItemStack[]{new ItemStack(item, amount)})); } public ConsumeItems consumeItems(ItemStack... items){ return consume(new ConsumeItems(items)); } public ConsumeCoolant consumeCoolant(float amount){ return consume(new ConsumeCoolant(amount)); } public ConsumeCoolant consumeCoolant(float amount, boolean allowLiquid, boolean allowGas){ return consume(new ConsumeCoolant(amount, allowLiquid, allowGas)); } public T consume(T consume){ if(consume instanceof ConsumePower){ //there can only be one power consumer consumeBuilder.removeAll(b -> b instanceof ConsumePower); consPower = (ConsumePower)consume; } consumeBuilder.add(consume); return consume; } public void setupRequirements(Category cat, ItemStack[] stacks){ requirements(cat, stacks); } public void setupRequirements(Category cat, BuildVisibility visible, ItemStack[] stacks){ requirements(cat, visible, stacks); } public void requirements(Category cat, ItemStack[] stacks, boolean unlocked){ requirements(cat, BuildVisibility.shown, stacks); this.alwaysUnlocked = unlocked; } public void requirements(Category cat, ItemStack[] stacks){ requirements(cat, BuildVisibility.shown, stacks); } /** Sets up requirements. Use only this method to set up requirements. */ public void requirements(Category cat, BuildVisibility visible, ItemStack[] stacks){ this.category = cat; this.requirements = stacks; this.buildVisibility = visible; Arrays.sort(requirements, Structs.comparingInt(i -> i.item.id)); } protected void initBuilding(){ //attempt to find the first declared class and use it as the entity type try{ Class current = getClass(); if(current.isAnonymousClass()){ current = current.getSuperclass(); } subclass = current; while(buildType == null && Block.class.isAssignableFrom(current)){ //first class that is subclass of Building Class type = Structs.find(current.getDeclaredClasses(), t -> Building.class.isAssignableFrom(t) && !t.isInterface()); if(type != null){ //these are inner classes, so they have an implicit parameter generated Constructor cons = (Constructor)type.getDeclaredConstructor(type.getDeclaringClass()); buildType = () -> { try{ return cons.newInstance(this); }catch(Exception e){ throw new RuntimeException(e); } }; } //scan through every superclass looking for it current = current.getSuperclass(); } }catch(Throwable ignored){ } if(buildType == null){ //assign default value buildType = Building::create; } } @Override public ItemStack[] researchRequirements(){ if(researchCost != null) return researchCost; if(researchCostMultiplier <= 0f) return ItemStack.empty; ItemStack[] out = new ItemStack[requirements.length]; for(int i = 0; i < out.length; i++){ int quantity = Mathf.round(60 * researchCostMultiplier + Mathf.pow(requirements[i].amount, 1.11f) * 20 * researchCostMultiplier * researchCostMultipliers.get(requirements[i].item, 1f), 10); out[i] = new ItemStack(requirements[i].item, UI.roundAmount(quantity)); } return out; } @Override public void getDependencies(Cons cons){ //just requires items for(ItemStack stack : requirements){ cons.get(stack.item); } //also requires inputs for(var c : consumeBuilder){ if(c.optional) continue; if(c instanceof ConsumeItems i){ for(ItemStack stack : i.items){ cons.get(stack.item); } } //TODO: requiring liquid dependencies is usually a bad idea, because there is no reason to pump/produce something until you actually need it. /*else if(c instanceof ConsumeLiquid i){ cons.get(i.liquid); }else if(c instanceof ConsumeLiquids i){ for(var stack : i.liquids){ cons.get(stack.liquid); } }*/ } } @Override public ContentType getContentType(){ return ContentType.block; } @Override public boolean logicVisible(){ return buildVisibility != BuildVisibility.hidden; } /** Called after all blocks are created. */ @Override @CallSuper public void init(){ //disable standard shadow if(customShadow){ hasShadow = false; } if(fogRadius > 0){ flags = flags.with(BlockFlag.hasFogRadius); } //initialize default health based on size if(health == -1){ boolean round = false; if(scaledHealth < 0){ scaledHealth = 40; float scaling = 1f; for(var stack : requirements){ scaling += stack.item.healthScaling; } scaledHealth *= scaling; round = true; } health = round ? Mathf.round(size * size * scaledHealth, 5) : (int)(size * size * scaledHealth); } clipSize = Math.max(clipSize, size * tilesize); if(hasLiquids && drawLiquidLight){ clipSize = Math.max(size * 30f * 2f, clipSize); } if(emitLight){ clipSize = Math.max(clipSize, lightRadius * 2f); } if(group == BlockGroup.transportation || category == Category.distribution){ acceptsItems = true; } offset = ((size + 1) % 2) * tilesize / 2f; sizeOffset = -((size - 1) / 2); if(requirements.length > 0){ buildCost = 0f; for(ItemStack stack : requirements){ buildCost += stack.amount * stack.item.cost; } } buildCost *= buildCostMultiplier; consumers = consumeBuilder.toArray(Consume.class); optionalConsumers = consumeBuilder.select(consume -> consume.optional && !consume.ignore()).toArray(Consume.class); nonOptionalConsumers = consumeBuilder.select(consume -> !consume.optional && !consume.ignore()).toArray(Consume.class); updateConsumers = consumeBuilder.select(consume -> consume.update && !consume.ignore()).toArray(Consume.class); hasConsumers = consumers.length > 0; itemFilter = new boolean[content.items().size]; liquidFilter = new boolean[content.liquids().size]; for(Consume cons : consumers){ cons.apply(this); } setBars(); stats.useCategories = true; //TODO check for double power consumption if(!logicConfigurable){ configurations.each((key, val) -> { if(UnlockableContent.class.isAssignableFrom(key)){ logicConfigurable = true; } }); } if(!outputsPower && consPower != null && consPower.buffered){ Log.warn("Consumer using buffered power: @. Disabling buffered power.", name); consPower.buffered = false; } if(buildVisibility == BuildVisibility.sandboxOnly){ hideDetails = false; } } @Override public void load(){ super.load(); region = Core.atlas.find(name); ContentRegions.loadRegions(this); //load specific team regions teamRegions = new TextureRegion[Team.all.length]; for(Team team : Team.all){ teamRegions[team.id] = teamRegion.found() && team.hasPalette ? Core.atlas.find(name + "-team-" + team.name, teamRegion) : teamRegion; } if(variants != 0){ variantRegions = new TextureRegion[variants]; for(int i = 0; i < variants; i++){ variantRegions[i] = Core.atlas.find(name + (i + 1)); } region = variantRegions[0]; if(customShadow){ variantShadowRegions = new TextureRegion[variants]; for(int i = 0; i < variants; i++){ variantShadowRegions[i] = Core.atlas.find(name + "-shadow" + (i + 1)); } } } } @Override public boolean isHidden(){ return !buildVisibility.visible() && !state.rules.revealedBlocks.contains(this); } @Override public void createIcons(MultiPacker packer){ super.createIcons(packer); if(!synthetic()){ PixmapRegion image = Core.atlas.getPixmap(fullIcon); mapColor.set(image.get(image.width/2, image.height/2)); } if(variants > 0){ for(int i = 0; i < variants; i++){ String rname = name + (i + 1); packer.add(PageType.editor, "editor-" + rname, Core.atlas.getPixmap(rname)); } } //generate paletted team regions if(teamRegion != null && teamRegion.found()){ for(Team team : Team.all){ //if there's an override, don't generate anything if(team.hasPalette && !Core.atlas.has(name + "-team-" + team.name)){ var base = Core.atlas.getPixmap(teamRegion); Pixmap out = new Pixmap(base.width, base.height); for(int x = 0; x < base.width; x++){ for(int y = 0; y < base.height; y++){ int color = base.get(x, y); int index = switch(color){ case 0xffffffff -> 0; case 0xdcc6c6ff, 0xdbc5c5ff -> 1; case 0x9d7f7fff, 0x9e8080ff -> 2; default -> -1; }; out.setRaw(x, y, index == -1 ? base.get(x, y) : team.palettei[index]); } } Drawf.checkBleed(out); packer.add(PageType.main, name + "-team-" + team.name, out); } } teamRegions = new TextureRegion[Team.all.length]; for(Team team : Team.all){ teamRegions[team.id] = teamRegion.found() && team.hasPalette ? Core.atlas.find(name + "-team-" + team.name, teamRegion) : teamRegion; } } Pixmap last = null; var gen = icons(); if(outlineIcon){ AtlasRegion atlasRegion = (AtlasRegion)gen[outlinedIcon >= 0 ? Math.min(outlinedIcon, gen.length - 1) : gen.length -1]; PixmapRegion region = Core.atlas.getPixmap(atlasRegion); Pixmap out = last = Pixmaps.outline(region, outlineColor, outlineRadius); Drawf.checkBleed(out); packer.add(PageType.main, atlasRegion.name, out); } var toOutline = new Seq(); getRegionsToOutline(toOutline); for(var region : toOutline){ if(region instanceof AtlasRegion atlas){ String regionName = atlas.name; Pixmap outlined = Pixmaps.outline(Core.atlas.getPixmap(region), outlineColor, outlineRadius); Drawf.checkBleed(outlined); packer.add(PageType.main, regionName + "-outline", outlined); } } PixmapRegion editorBase; if(gen.length > 1){ Pixmap base = Core.atlas.getPixmap(gen[0]).crop(); for(int i = 1; i < gen.length; i++){ if(i == gen.length - 1 && last != null){ base.draw(last, 0, 0, true); }else{ base.draw(Core.atlas.getPixmap(gen[i]), true); } } packer.add(PageType.main, "block-" + name + "-full", base); editorBase = new PixmapRegion(base); }else{ if(gen[0] != null) packer.add(PageType.main, "block-" + name + "-full", Core.atlas.getPixmap(gen[0])); editorBase = gen[0] == null ? Core.atlas.getPixmap(fullIcon) : Core.atlas.getPixmap(gen[0]); } packer.add(PageType.editor, name + "-icon-editor", editorBase); } public int planRotation(int rot){ return !rotate && lockRotation ? 0 : rot; } public void flipRotation(BuildPlan req, boolean x){ if((x == (req.rotation % 2 == 0)) != invertFlip){ req.rotation = planRotation(Mathf.mod(req.rotation + 2, 4)); } } @Override public double sense(LAccess sensor){ return switch(sensor){ case color -> mapColor.toDoubleBits(); case health, maxHealth -> health; case size -> size; case itemCapacity -> itemCapacity; case liquidCapacity -> liquidCapacity; case powerCapacity -> consPower != null && consPower.buffered ? consPower.capacity : 0f; case id -> getLogicId(); default -> Double.NaN; }; } @Override public double sense(Content content){ return Double.NaN; } @Override public Object senseObject(LAccess sensor){ if(sensor == LAccess.name) return name; return noSensed; } }