diff --git a/res_raw/sprites/blueprints/belt_left.png b/res_raw/sprites/blueprints/belt_left.png new file mode 100644 index 00000000..476a17e2 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_left.png differ diff --git a/res_raw/sprites/blueprints/belt_right.png b/res_raw/sprites/blueprints/belt_right.png new file mode 100644 index 00000000..77ba6a33 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_right.png differ diff --git a/res_raw/sprites/blueprints/belt_top.png b/res_raw/sprites/blueprints/belt_top.png new file mode 100644 index 00000000..d8532b64 Binary files /dev/null and b/res_raw/sprites/blueprints/belt_top.png differ diff --git a/res_raw/sprites/blueprints/cutter.png b/res_raw/sprites/blueprints/cutter.png new file mode 100644 index 00000000..43d56cb2 Binary files /dev/null and b/res_raw/sprites/blueprints/cutter.png differ diff --git a/res_raw/sprites/blueprints/miner.png b/res_raw/sprites/blueprints/miner.png new file mode 100644 index 00000000..f07d0276 Binary files /dev/null and b/res_raw/sprites/blueprints/miner.png differ diff --git a/res_raw/sprites/blueprints/mixer.png b/res_raw/sprites/blueprints/mixer.png new file mode 100644 index 00000000..2937c38d Binary files /dev/null and b/res_raw/sprites/blueprints/mixer.png differ diff --git a/res_raw/sprites/blueprints/painter.png b/res_raw/sprites/blueprints/painter.png new file mode 100644 index 00000000..4feefe33 Binary files /dev/null and b/res_raw/sprites/blueprints/painter.png differ diff --git a/res_raw/sprites/blueprints/rotater.png b/res_raw/sprites/blueprints/rotater.png new file mode 100644 index 00000000..c986d8df Binary files /dev/null and b/res_raw/sprites/blueprints/rotater.png differ diff --git a/res_raw/sprites/blueprints/splitter.png b/res_raw/sprites/blueprints/splitter.png new file mode 100644 index 00000000..984a99a8 Binary files /dev/null and b/res_raw/sprites/blueprints/splitter.png differ diff --git a/res_raw/sprites/blueprints/stacker.png b/res_raw/sprites/blueprints/stacker.png new file mode 100644 index 00000000..f11116b4 Binary files /dev/null and b/res_raw/sprites/blueprints/stacker.png differ diff --git a/res_raw/sprites/blueprints/trash.png b/res_raw/sprites/blueprints/trash.png new file mode 100644 index 00000000..99ca2340 Binary files /dev/null and b/res_raw/sprites/blueprints/trash.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_entry.png b/res_raw/sprites/blueprints/underground_belt_entry.png new file mode 100644 index 00000000..aa237b6d Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_entry.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_exit.png b/res_raw/sprites/blueprints/underground_belt_exit.png new file mode 100644 index 00000000..760f63a9 Binary files /dev/null and b/res_raw/sprites/blueprints/underground_belt_exit.png differ diff --git a/res_raw/sprites/create_blueprint_previews.py b/res_raw/sprites/create_blueprint_previews.py new file mode 100644 index 00000000..4dd44445 --- /dev/null +++ b/res_raw/sprites/create_blueprint_previews.py @@ -0,0 +1,96 @@ +# Requirements: numpy, scipy, Pillow, + +import sys +import numpy as np +from scipy import ndimage +from PIL import Image, ImageFilter, ImageChops +import math +from os import listdir +from os.path import isdir, isfile + +roberts_cross_v = np.array([[0, 0, 0], + [0, 1, 0], + [0, 0, -1]]) + +roberts_cross_h = np.array([[0, 0, 0], + [0, 0, 1], + [0, -1, 0]]) + + +def rgb2gray(rgb): + return np.dot(rgb[..., :3], [0.2989, 0.5870, 0.1140]) + + +def save_image(data, outfilename, src_image): + img = Image.fromarray(np.asarray( + np.clip(data, 0, 255), dtype="uint8"), "L") + dest = Image.new("RGBA", (img.width, img.height)) + src = img.load() + dst = dest.load() + + realSrc = src_image.load() + mask = src_image.filter(ImageFilter.GaussianBlur(10)).load() + orig = src_image.load() + + for x in range(img.width): + for y in range(img.height): + realpixl = realSrc[x, y] + greyval = float(src[x, y]) + greyval = min(255.0, greyval) + greyval = math.pow( + min(1, float(greyval / 255.0 * 1)), 1.5) * 255.0 * 1 + greyval = max(0, greyval) + alpha = mask[x, y][3] / 255.0 * 1 + + edgeFactor = src[x, y] / 255.0 + noEdge = 1 - edgeFactor + + shadow = min(1, 1 - realpixl[3] / 255.0 - edgeFactor) + noShadow = 1 - shadow + + dst[x, y] = ( + min(255, int((realpixl[0] / 255.0 * 0.4 + 0.6) * 104 * 1.1)), + min(255, int((realpixl[1] / 255.0 * 0.4 + 0.6) * 200 * 1.1)), + min(255, int((realpixl[2] / 255.0 * 0.4 + 0.6) * 255 * 1.1)), + min(255, int(float(realpixl[3]) * (0.6 + 5 * edgeFactor)))) + + + dest.save(outfilename) + + +def roberts_cross(infilename, outfilename): + print "Processing", infilename + img = Image.open(infilename) + img.load() + img = img.filter(ImageFilter.GaussianBlur(0.5)) + + image = rgb2gray(np.asarray(img, dtype="int32")) + vertical = ndimage.convolve(image, roberts_cross_v) + horizontal = ndimage.convolve(image, roberts_cross_h) + output_image = np.sqrt(np.square(horizontal) + np.square(vertical)) + save_image(output_image, outfilename, img) + + +def generateUiPreview(srcPath, buildingId): + print srcPath, buildingId + img = Image.open(srcPath) + img.load() + img.thumbnail((110, 110), Image.ANTIALIAS) + img.save("../res/ui/hud/building_previews/" + buildingId + ".png") + + img = img.convert("LA") + + data = img.load() + for x in range(img.width): + for y in range(img.height): + data[x, y] = (data[x, y][0], int(data[x, y][1] * 0.5)) + + img.save("../res/ui/hud/building_previews/" + buildingId + "_disabled.png") + + +buildings = listdir("buildings") + +for buildingId in buildings: + if "hub" in buildingId: + continue + roberts_cross("buildings/" + buildingId + "", "blueprints/" + buildingId + "") diff --git a/res_raw/sprites/misc/slot_good_arrow.png b/res_raw/sprites/misc/slot_good_arrow.png index 760ab171..3001e8b4 100644 Binary files a/res_raw/sprites/misc/slot_good_arrow.png and b/res_raw/sprites/misc/slot_good_arrow.png differ diff --git a/src/js/core/rectangle.js b/src/js/core/rectangle.js index e736f9bd..85fec31d 100644 --- a/src/js/core/rectangle.js +++ b/src/js/core/rectangle.js @@ -175,13 +175,14 @@ export class Rectangle { return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor); } - // Increases the rectangle in all directions - expandInAllDirections(amount) { - this.x -= amount; - this.y -= amount; - this.w += 2 * amount; - this.h += 2 * amount; - return this; + /** + * Expands the rectangle in all directions + * @param {number} amount + * @returns {Rectangle} new rectangle + */ + + expandedInAllDirections(amount) { + return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount); } // Culling helpers diff --git a/src/js/game/buildings/belt_base.js b/src/js/game/buildings/belt_base.js index f021f74e..74244dcf 100644 --- a/src/js/game/buildings/belt_base.js +++ b/src/js/game/buildings/belt_base.js @@ -19,6 +19,52 @@ export class MetaBeltBaseBuilding extends MetaBuilding { return "#777"; } + getName() { + return "Belt"; + } + + getDescription() { + return "Transports items, hold and drag to place multiple, press 'R' to rotate."; + } + + getPreviewSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/buildings/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/buildings/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/buildings/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getBlueprintSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/blueprints/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/blueprints/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/blueprints/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getStayInPlacementMode() { + return true; + } + /** * Creates the entity at the given location * @param {Entity} entity @@ -140,65 +186,29 @@ export class MetaBeltBaseBuilding extends MetaBuilding { // When there is a top acceptor, ignore sides // NOTICE: This makes the belt prefer side turns *way* too much! - // if (!hasTopAcceptor) { - // // When there is an acceptor to the right but no acceptor to the left, - // // do a turn to the right - // if (hasRightAcceptor && !hasLeftAcceptor) { - // return { - // rotation, - // rotationVariant: 2, - // }; - // } - - // // When there is an acceptor to the left but no acceptor to the right, - // // do a turn to the left - // if (hasLeftAcceptor && !hasRightAcceptor) { - // return { - // rotation, - // rotationVariant: 1, - // }; - // } - // } + if (!hasTopAcceptor) { + // When there is an acceptor to the right but no acceptor to the left, + // do a turn to the right + if (hasRightAcceptor && !hasLeftAcceptor) { + return { + rotation, + rotationVariant: 2, + }; + } + + // When there is an acceptor to the left but no acceptor to the right, + // do a turn to the left + if (hasLeftAcceptor && !hasRightAcceptor) { + return { + rotation, + rotationVariant: 1, + }; + } + } return { rotation, rotationVariant: 0, }; } - - getName() { - return "Belt"; - } - - getDescription() { - return "Transports items, hold and drag to place multiple, press 'R' to rotate."; - } - - getPreviewSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/belt/forward_0.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/belt/left_0.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/belt/right_0.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } - - getStayInPlacementMode() { - return true; - } - - /** - * Can be overridden - */ - internalGetBeltDirection(rotationVariant) { - return enumDirection.top; - } } diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 31021c6e..c00a9469 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -51,6 +51,17 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { } } + getBlueprintSprite(rotationVariant) { + switch (arrayUndergroundRotationVariantToMode[rotationVariant]) { + case enumUndergroundBeltMode.sender: + return Loader.getSprite("sprites/blueprints/underground_belt_entry.png"); + case enumUndergroundBeltMode.receiver: + return Loader.getSprite("sprites/blueprints/underground_belt_exit.png"); + default: + assertAlways(false, "Invalid rotation variant"); + } + } + /** * @param {GameRoot} root */ diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index bb256dc6..2f44135e 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -13,7 +13,7 @@ import { import { pulseAnimation, makeDiv } from "../../../core/utils"; import { DynamicDomAttach } from "../dynamic_dom_attach"; import { TrackedState } from "../../../core/tracked_state"; -import { Math_abs, Math_radians } from "../../../core/builtins"; +import { Math_abs, Math_radians, Math_degrees } from "../../../core/builtins"; import { Loader } from "../../../core/loader"; import { drawRotatedSprite } from "../../../core/draw_utils"; import { Entity } from "../../entity"; @@ -96,6 +96,9 @@ export class HUDBuildingPlacer extends BaseHUDPart { if (!oldPos.equals(newPos)) { const delta = newPos.sub(oldPos); + const angleDeg = Math_degrees(delta.angle()); + this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; + // - Using bresenhams algorithmus let x0 = oldPos.x; @@ -351,36 +354,49 @@ export class HUDBuildingPlacer extends BaseHUDPart { metaBuilding.updateRotationVariant(this.fakeEntity, rotationVariant); // Check if we could place the buildnig - const canBuild = this.root.logic.checkCanPlaceBuilding(tile, rotation, metaBuilding); + const canBuild = this.root.logic.checkCanPlaceBuilding({ + origin: tile, + rotation, + rotationVariant, + building: metaBuilding, + }); + + // Fade in / out + parameters.context.lineWidth = 1; + // parameters.context.globalAlpha = 0.3 + pulseAnimation(this.root.time.realtimeNow(), 0.9) * 0.7; // Determine the bounds and visualize them const entityBounds = staticComp.getTileSpaceBounds(); - const drawBorder = 2; - parameters.context.globalAlpha = 0.5; + const drawBorder = -3; if (canBuild) { - parameters.context.fillStyle = "rgba(0, 255, 0, 0.2)"; + parameters.context.strokeStyle = "rgba(56, 235, 111, 0.5)"; + parameters.context.fillStyle = "rgba(56, 235, 111, 0.2)"; } else { + parameters.context.strokeStyle = "rgba(255, 0, 0, 0.2)"; parameters.context.fillStyle = "rgba(255, 0, 0, 0.2)"; } - parameters.context.fillRect( + + parameters.context.beginRoundedRect( entityBounds.x * globalConfig.tileSize - drawBorder, entityBounds.y * globalConfig.tileSize - drawBorder, entityBounds.w * globalConfig.tileSize + 2 * drawBorder, - entityBounds.h * globalConfig.tileSize + 2 * drawBorder + entityBounds.h * globalConfig.tileSize + 2 * drawBorder, + 4 ); - - // Draw ejectors - if (canBuild) { - this.drawMatchingAcceptorsAndEjectors(parameters); - } + parameters.context.stroke(); + // parameters.context.fill(); + parameters.context.globalAlpha = 1; // HACK to draw the entity sprite - const previewSprite = metaBuilding.getPreviewSprite(rotationVariant); - parameters.context.globalAlpha = 0.8 + pulseAnimation(this.root.time.realtimeNow(), 1) * 0.1; + const previewSprite = metaBuilding.getBlueprintSprite(rotationVariant); staticComp.origin = worldPos.divideScalar(globalConfig.tileSize).subScalars(0.5, 0.5); staticComp.drawSpriteOnFullEntityBounds(parameters, previewSprite); staticComp.origin = tile; - parameters.context.globalAlpha = 1; + + // Draw ejectors + if (canBuild) { + this.drawMatchingAcceptorsAndEjectors(parameters); + } } /** @@ -397,6 +413,8 @@ export class HUDBuildingPlacer extends BaseHUDPart { // Just ignore this code ... + const offsetShift = 10; + if (acceptorComp) { const slots = acceptorComp.slots; for (let acceptorSlotIndex = 0; acceptorSlotIndex < slots.length; ++acceptorSlotIndex) { @@ -437,7 +455,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { y: acceptorSlotWsPos.y, angle: Math_radians(enumDirectionToAngle[enumInvertedDirections[worldDirection]]), size: 13, - offsetY: 15, + offsetY: offsetShift + 13, }); parameters.context.globalAlpha = 1; } @@ -483,7 +501,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { y: ejectorSLotWsPos.y, angle: Math_radians(enumDirectionToAngle[ejectorSlotWsDirection]), size: 13, - offsetY: 15, + offsetY: offsetShift, }); parameters.context.globalAlpha = 1; } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index fe58ee4e..839549b7 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -6,6 +6,7 @@ import { StaticMapEntityComponent } from "./components/static_map_entity"; import { Math_abs } from "../core/builtins"; import { Rectangle } from "../core/rectangle"; import { createLogger } from "../core/logging"; +import { MetaBeltBaseBuilding, arrayBeltVariantToRotation } from "./buildings/belt_base"; const logger = createLogger("ingame/logic"); @@ -46,12 +47,14 @@ export class GameLogic { } /** - * - * @param {Vector} origin - * @param {number} rotation - * @param {MetaBuilding} building + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {MetaBuilding} param0.building + * @returns {boolean} */ - isAreaFreeToBuild(origin, rotation, building) { + isAreaFreeToBuild({ origin, rotation, rotationVariant, building }) { const checker = new StaticMapEntityComponent({ origin, tileSize: building.getDimensions(), @@ -63,8 +66,19 @@ export class GameLogic { for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { const contents = this.root.map.getTileContentXY(x, y); - if (contents && !contents.components.ReplaceableMapEntity) { - return false; + if (contents) { + if ( + !this.checkCanReplaceBuilding({ + original: contents, + origin, + building, + rotation, + rotationVariant, + }) + ) { + // Content already has same rotation + return false; + } } } } @@ -72,16 +86,54 @@ export class GameLogic { } /** - * - * @param {Vector} origin - * @param {number} rotation - * @param {MetaBuilding} building + * Checks if the given building can be replaced by another + * @param {object} param0 + * @param {Entity} param0.original + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {MetaBuilding} param0.building + * @returns {boolean} + */ + checkCanReplaceBuilding({ original, origin, building, rotation, rotationVariant }) { + if (!original.components.ReplaceableMapEntity) { + // Can not get replaced at all + return false; + } + + const staticComp = original.components.StaticMapEntity; + assert(staticComp, "Building is not static"); + const beltComp = original.components.Belt; + if (beltComp) { + // Its a belt, check if it differs in either rotation or rotation variant + if (staticComp.rotationDegrees !== rotation) { + return true; + } + if (beltComp.direction !== arrayBeltVariantToRotation[rotationVariant]) { + return true; + } + } + + return false; + } + + /** + * @param {object} param0 + * @param {Vector} param0.origin + * @param {number} param0.rotation + * @param {number} param0.rotationVariant + * @param {MetaBuilding} param0.building */ - checkCanPlaceBuilding(origin, rotation, building) { + checkCanPlaceBuilding({ origin, rotation, rotationVariant, building }) { if (!building.getIsUnlocked(this.root)) { return false; } - return this.isAreaFreeToBuild(origin, rotation, building); + return this.isAreaFreeToBuild({ + origin, + rotation, + rotationVariant, + building, + }); } /** @@ -93,7 +145,7 @@ export class GameLogic { * @param {MetaBuilding} param0.building */ tryPlaceBuilding({ origin, rotation, rotationVariant, building }) { - if (this.checkCanPlaceBuilding(origin, rotation, building)) { + if (this.checkCanPlaceBuilding({ origin, rotation, rotationVariant, building })) { // Remove any removeable entities below const checker = new StaticMapEntityComponent({ origin, @@ -106,7 +158,7 @@ export class GameLogic { for (let x = rect.x; x < rect.x + rect.w; ++x) { for (let y = rect.y; y < rect.y + rect.h; ++y) { const contents = this.root.map.getTileContentXY(x, y); - if (contents && contents.components.ReplaceableMapEntity) { + if (contents) { if (!this.tryDeleteBuilding(contents)) { logger.error("Building has replaceable component but is also unremovable"); return false; diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index a5fc8c25..a2201bed 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -65,6 +65,14 @@ export class MetaBuilding { return Loader.getSprite("sprites/buildings/" + this.id + ".png"); } + /** + * Returns a sprite for blueprints + * @returns {AtlasSprite} + */ + getBlueprintSprite(rotationVariant = 0) { + return Loader.getSprite("sprites/blueprints/" + this.id + ".png"); + } + /** * Returns whether this building is rotateable * @returns {boolean} @@ -110,8 +118,8 @@ export class MetaBuilding { this.setupEntityComponents(entity, root); this.updateRotationVariant(entity, rotationVariant); - root.entityMgr.registerEntity(entity); root.map.placeStaticEntity(entity); + root.entityMgr.registerEntity(entity); return entity; } diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index a2853998..e71da85c 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -8,6 +8,8 @@ import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector"; import { MapChunkView } from "../map_chunk_view"; +import { gMetaBuildingRegistry } from "../../core/global_registries"; +import { MetaBeltBaseBuilding } from "../buildings/belt_base"; const BELT_ANIM_COUNT = 6; @@ -51,6 +53,49 @@ export class BeltSystem extends GameSystemWithFilter { Loader.getSprite("sprites/belt/right_5.png"), ], }; + + this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); + this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); + } + + /** + * Updates the belt placement after an entity has been added / deleted + * @param {Entity} entity + */ + updateSurroundingBeltPlacement(entity) { + const staticComp = entity.components.StaticMapEntity; + if (!staticComp) { + return; + } + + const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); + + // Compute affected area + const originalRect = staticComp.getTileSpaceBounds(); + const affectedArea = originalRect.expandedInAllDirections(1); + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { + for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { + if (!originalRect.containsPoint(x, y)) { + const targetEntity = this.root.map.getTileContentXY(x, y); + if (targetEntity) { + const targetBeltComp = targetEntity.components.Belt; + if (targetBeltComp) { + const targetStaticComp = targetEntity.components.StaticMapEntity; + const { + rotation, + rotationVariant, + } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile( + this.root, + new Vector(x, y), + targetStaticComp.rotationDegrees + ); + targetStaticComp.rotationDegrees = rotation; + metaBelt.updateRotationVariant(targetEntity, rotationVariant); + } + } + } + } + } } draw(parameters) {