Better fog / Research cost tweaks

This commit is contained in:
Anuken 2022-02-19 16:24:22 -05:00
parent c5ec8ff3ce
commit 895fa784cf
17 changed files with 158 additions and 117 deletions

View file

@ -8,5 +8,5 @@ varying vec2 v_texCoords;
void main(){
vec4 color = texture2D(u_texture, v_texCoords.xy);
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0 - floor(color.r / QUANT) * QUANT);
gl_FragColor = vec4(1.0, 1.0, 1.0, (1.0 - floor(color.r / QUANT) * QUANT) * (step(color.r, 0.99))) * v_color;
}

View file

@ -2,8 +2,8 @@ package mindustry.ai.types;
import arc.math.*;
import arc.math.geom.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
@ -35,7 +35,7 @@ public class HugAI extends AIController{
}
//raycast for target
if(target != null && unit.within(target, unit.type.range) && !Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
if(target != null && unit.within(target, unit.type.range) && !World.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
for(Point2 p : Geometry.d4c){
if(!unit.canPass(x + p.x, y + p.y)){
return true;

View file

@ -3,6 +3,7 @@ package mindustry.ai.types;
import arc.math.geom.*;
import mindustry.*;
import mindustry.ai.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.gen.*;
import mindustry.world.*;
@ -48,7 +49,7 @@ public class SuicideAI extends GroundAI{
blockedByBlock = false;
//raycast for target
boolean blocked = Vars.world.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
boolean blocked = World.raycast(unit.tileX(), unit.tileY(), target.tileX(), target.tileY(), (x, y) -> {
for(Point2 p : Geometry.d4c){
Tile tile = Vars.world.tile(x + p.x, y + p.y);
if(tile != null && tile.build == target) return false;

View file

@ -925,6 +925,7 @@ public class Blocks{
envDisabled = Env.none;
itemCapacity = 30;
drawer = new DrawArcSmelter();
researchCostMultiplier = 0.3f;
consumeItems(with(Items.graphite, 1, Items.sand, 4));
consumePower(6f);
@ -2481,7 +2482,7 @@ public class Blocks{
cliffCrusher = new WallCrafter("cliff-crusher"){{
requirements(Category.production, with(Items.graphite, 25, Items.beryllium, 20));
consumePower(0.8f);
consumePower(11 / 60f);
drillTime = 110f;
size = 2;

View file

@ -375,54 +375,6 @@ public class World{
if(invalidMap) Core.app.post(() -> state.set(State.menu));
}
public void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
}
public void raycastEach(int x1, int y1, int x2, int y2, Raycaster cons){
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
int e2, err = dx - dy;
while(true){
if(cons.accept(x, y)) break;
if(x == x2 && y == y2) break;
e2 = 2 * err;
if(e2 > -dy){
err -= dy;
x += sx;
}
if(e2 < dx){
err += dx;
y += sy;
}
}
}
public boolean raycast(int x1, int y1, int x2, int y2, Raycaster cons){
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
int e2, err = dx - dy;
while(true){
if(cons.accept(x, y)) return true;
if(x == x2 && y == y2) return false;
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x = x + sx;
}
if(e2 < dx){
err = err + dx;
y = y + sy;
}
}
}
public void addDarkness(Tiles tiles){
byte[] dark = new byte[tiles.width * tiles.height];
byte[] writeBuffer = new byte[tiles.width * tiles.height];
@ -547,6 +499,54 @@ public class World{
return dark;
}
public static void raycastEachWorld(float x0, float y0, float x1, float y1, Raycaster cons){
raycastEach(toTile(x0), toTile(y0), toTile(x1), toTile(y1), cons);
}
public static void raycastEach(int x1, int y1, int x2, int y2, Raycaster cons){
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
int e2, err = dx - dy;
while(true){
if(cons.accept(x, y)) break;
if(x == x2 && y == y2) break;
e2 = 2 * err;
if(e2 > -dy){
err -= dy;
x += sx;
}
if(e2 < dx){
err += dx;
y += sy;
}
}
}
public static boolean raycast(int x1, int y1, int x2, int y2, Raycaster cons){
int x = x1, dx = Math.abs(x2 - x), sx = x < x2 ? 1 : -1;
int y = y1, dy = Math.abs(y2 - y), sy = y < y2 ? 1 : -1;
int e2, err = dx - dy;
while(true){
if(cons.accept(x, y)) return true;
if(x == x2 && y == y2) return false;
e2 = 2 * err;
if(e2 > -dy){
err = err - dy;
x = x + sx;
}
if(e2 < dx){
err = err + dx;
y = y + sy;
}
}
}
private class Context implements WorldContext{
Context(){}

View file

@ -125,7 +125,7 @@ public class Damage{
public static @Nullable Building findAbsorber(Team team, float x1, float y1, float x2, float y2){
tmpBuilding = null;
boolean found = world.raycast(World.toTile(x1), World.toTile(y1), World.toTile(x2), World.toTile(y2),
boolean found = World.raycast(World.toTile(x1), World.toTile(y1), World.toTile(x2), World.toTile(y2),
(x, y) -> (tmpBuilding = world.build(x, y)) != null && tmpBuilding.team != team && tmpBuilding.block.absorbLasers);
return found ? tmpBuilding : null;
@ -136,7 +136,7 @@ public class Damage{
furthest = null;
boolean found = world.raycast(b.tileX(), b.tileY(), World.toTile(b.x + Tmp.v1.x), World.toTile(b.y + Tmp.v1.y),
boolean found = World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + Tmp.v1.x), World.toTile(b.y + Tmp.v1.y),
(x, y) -> (furthest = world.tile(x, y)) != null && furthest.team() != b.team && (furthest.build != null && furthest.build.absorbLasers()));
return found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length;
@ -148,7 +148,7 @@ public class Damage{
furthest = null;
pierceCount = 0;
boolean found = world.raycast(b.tileX(), b.tileY(), World.toTile(b.x + Tmp.v1.x), World.toTile(b.y + Tmp.v1.y),
boolean found = World.raycast(b.tileX(), b.tileY(), World.toTile(b.x + Tmp.v1.x), World.toTile(b.y + Tmp.v1.y),
(x, y) -> (furthest = world.tile(x, y)) != null && furthest.build != null && furthest.team() != b.team && ++pierceCount >= pierceCap);
return found && furthest != null ? Math.max(6f, b.dst(furthest.worldx(), furthest.worldy())) : length;
@ -222,7 +222,7 @@ public class Damage{
if(hitter.type.collidesGround){
seg1.set(x, y);
seg2.set(seg1).add(tr);
world.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {
World.raycastEachWorld(x, y, seg2.x, seg2.y, (cx, cy) -> {
collider.get(cx, cy);
for(Point2 p : Geometry.d4){
@ -294,7 +294,7 @@ public class Damage{
tmpBuilding = null;
if(hitter.type.collidesGround){
world.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
World.raycastEachWorld(x, y, x + tr.x, y + tr.y, (cx, cy) -> {
Building tile = world.build(cx, cy);
if(tile != null && tile.team != hitter.team){
tmpBuilding = tile;

View file

@ -50,7 +50,7 @@ public class Lightning{
bhit = false;
Vec2 from = lines.get(lines.size - 2);
Vec2 to = lines.get(lines.size - 1);
world.raycastEach(World.toTile(from.getX()), World.toTile(from.getY()), World.toTile(to.getX()), World.toTile(to.getY()), (wx, wy) -> {
World.raycastEach(World.toTile(from.getX()), World.toTile(from.getY()), World.toTile(to.getX()), World.toTile(to.getY()), (wx, wy) -> {
Tile tile = world.tile(wx, wy);
if(tile != null && (tile.build != null && tile.build.isInsulated()) && tile.team() != team){

View file

@ -18,6 +18,11 @@ abstract class TeamComp implements Posc{
return team.rules().cheat;
}
/** @return whether the center of this entity is visible to the viewing team. */
boolean inFogTo(Team viewer){
return this.team != viewer && !fogControl.isVisible(viewer, x, y);
}
@Nullable
public CoreBuild core(){
return team.core();

View file

@ -6,6 +6,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.annotations.Annotations.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import mindustry.io.SaveFileReader.*;
@ -63,7 +64,7 @@ public class FogControl implements CustomChunk{
if(state.rules.fog && event.tile.build != null && event.tile.isCenter() && !event.tile.build.team.isAI() && event.tile.block().flags.contains(BlockFlag.hasFogRadius)){
var data = data(event.tile.team());
if(data != null){
data.dynamicUpdated = data.dynamicUpdatedClient = true;
data.dynamicUpdated = true;
}
synchronized(staticEvents){
@ -78,7 +79,7 @@ public class FogControl implements CustomChunk{
if(state.rules.fog && e.tile.build != null && !e.tile.build.team.isAI() && e.tile.block().flags.contains(BlockFlag.hasFogRadius)){
var data = data(e.tile.team());
if(data != null){
data.dynamicUpdated = data.dynamicUpdatedClient = true;
data.dynamicUpdated = true;
}
}
});
@ -90,11 +91,16 @@ public class FogControl implements CustomChunk{
return fog == null || fog[team.id] == null ? null : fog[team.id].staticData;
}
public boolean isVisible(Team team, int x, int y){
public boolean isVisible(Team team, float x, float y){
return isVisibleTile(team, World.toTile(x), World.toTile(y));
}
public boolean isVisibleTile(Team team, int x, int y){
if(!state.rules.fog) return true;
var data = data(team);
if(data == null || x < 0 || y < 0 || x >= ww || y >= wh) return false;
if(data == null) return true;
if(x < 0 || y < 0 || x >= ww || y >= wh) return false;
return data.read.get(x + y * ww);
}
@ -161,7 +167,7 @@ public class FogControl implements CustomChunk{
if(data == null){
data = fog[team.team.id] = new FogData();
data.dynamicUpdated = data.dynamicUpdatedClient = true;
data.dynamicUpdated = true;
}
synchronized(staticEvents){
@ -176,27 +182,21 @@ public class FogControl implements CustomChunk{
if(unit.lastFogPos != pos){
pushEvent(event);
unit.lastFogPos = pos;
data.dynamicUpdated = data.dynamicUpdatedClient = true;
data.dynamicUpdated = true;
}
}
}
//add building updates (TODO this can run in the if-check, but the renderer needs it...)
for(var build : indexer.getFlagged(team.team, BlockFlag.hasFogRadius)){
unitEventQueue.add(FogEvent.get(build.tile.x, build.tile.y, build.block.fogRadius, 0));
}
//on the client, let the renderer know of all the fog sources
if(data.dynamicUpdatedClient && !headless && team.team == Vars.player.team()){
renderer.fog.flushDynamic(unitEventQueue);
data.dynamicUpdatedClient = false;
}
//if it's time for an update, flush *everything* onto the update queue
if(data.dynamicUpdated && Time.timeSinceMillis(data.lastDynamicMs) > staticUpdateInterval){
data.dynamicUpdated = false;
data.lastDynamicMs = Time.millis();
//add building updates
for(var build : indexer.getFlagged(team.team, BlockFlag.hasFogRadius)){
dynamicEventQueue.add(FogEvent.get(build.tile.x, build.tile.y, build.block.fogRadius, 0));
}
//add unit updates
dynamicEventQueue.addAll(unitEventQueue);
}
@ -405,8 +405,7 @@ public class FogControl implements CustomChunk{
int consec = data & 0b0111_1111;
if(sign){
//TODO disabled for testing?
//bools.set(pos, pos + consec);
bools.set(pos, pos + consec);
pos += consec;
}else{
pos += consec;
@ -480,8 +479,6 @@ public class FogControl implements CustomChunk{
/** if true, a dynamic fog update must be scheduled. */
boolean dynamicUpdated;
boolean dynamicUpdatedClient;
FogData(){
int len = ww * wh;

View file

@ -12,15 +12,17 @@ import mindustry.game.EventType.*;
import mindustry.game.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.meta.*;
import static mindustry.Vars.*;
/** Highly experimental fog-of-war renderer. */
public class FogRenderer{
public static final Color
staticColor = new Color(0f, 0f, 0f, 1f),
dynamicColor = new Color(0f, 0f, 0f, 0.5f);
private FrameBuffer staticFog = new FrameBuffer(), dynamicFog = new FrameBuffer();
private LongSeq events = new LongSeq();
private LongSeq dynamics = new LongSeq();
private boolean dynamicUpdate = false;
private Rect rect = new Rect();
private @Nullable Team lastTeam;
@ -35,12 +37,6 @@ public class FogRenderer{
events.add(event);
}
public void flushDynamic(LongSeq seq){
dynamics.clear();
dynamics.addAll(seq);
dynamicUpdate = true;
}
public Texture getStaticTexture(){
return staticFog.getTexture();
}
@ -54,25 +50,33 @@ public class FogRenderer{
if(fogControl.getDiscovered(player.team()) == null) return;
//resize if world size changes
boolean
clearStatic = staticFog.resizeCheck(world.width(), world.height()),
clearDynamic = dynamicFog.resizeCheck(world.width(), world.height());
boolean clearStatic = staticFog.resizeCheck(world.width(), world.height());
dynamicFog.resize(world.width(), world.height());
if(player.team() != lastTeam){
copyFromCpu();
lastTeam = player.team();
clearStatic = false;
dynamicUpdate = true;
}
if(clearDynamic || dynamicUpdate){
dynamicUpdate = false;
//draw dynamic fog every frame
{
Draw.proj(0, 0, staticFog.getWidth(), staticFog.getHeight());
Core.camera.bounds(Tmp.r1);
Draw.proj(0, 0, staticFog.getWidth() * tilesize, staticFog.getHeight() * tilesize);
dynamicFog.begin(Color.black);
ScissorStack.push(rect.set(1, 1, staticFog.getWidth() - 2, staticFog.getHeight() - 2));
//TODO render all (clipped) view circles
Team team = player.team();
for(var build : indexer.getFlagged(team, BlockFlag.hasFogRadius)){
poly(Tmp.r1, build.x, build.y, build.block.fogRadius * tilesize);
}
for(var unit : team.data().units){
poly(Tmp.r1, unit.x, unit.y, unit.type.fogRadius * tilesize);
}
dynamicFog.end();
ScissorStack.pop();
@ -95,18 +99,10 @@ public class FogRenderer{
Draw.color(Color.white);
//process new fog events
//process new static fog events
for(int i = 0; i < events.size; i++){
long e = events.items[i];
Tile tile = world.tile(FogEvent.x(e), FogEvent.y(e));
float o = 0f;
//visual offset for uneven blocks; this is not reflected on the CPU, but it doesn't really matter
if(tile != null && tile.block().size % 2 == 0 && tile.isCenter()){
o = 0.5f;
}
Fill.poly(FogEvent.x(e) + 0.5f + o, FogEvent.y(e) + 0.5f + o, 20, FogEvent.radius(e) + 0.3f);
renderEvent(events.items[i]);
}
events.clear();
staticFog.end();
@ -115,12 +111,32 @@ public class FogRenderer{
}
staticFog.getTexture().setFilter(TextureFilter.linear);
dynamicFog.getTexture().setFilter(TextureFilter.linear);
Draw.shader(Shaders.fog);
Draw.color(dynamicColor);
Draw.fbo(dynamicFog.getTexture(), world.width(), world.height(), tilesize);
Draw.color(staticColor);
Draw.fbo(staticFog.getTexture(), world.width(), world.height(), tilesize);
Draw.shader();
}
void poly(Rect check, float x, float y, float rad){
if(check.overlaps(x - rad / 2f, y - rad / 2f, rad * 2f, rad * 2f)){
Fill.poly(x, y, 20, rad);
}
}
void renderEvent(long e){
Tile tile = world.tile(FogEvent.x(e), FogEvent.y(e));
float o = 0f;
//visual offset for uneven blocks; this is not reflected on the CPU, but it doesn't really matter
if(tile != null && tile.block().size % 2 == 0 && tile.isCenter()){
o = 0.5f;
}
Fill.poly(FogEvent.x(e) + 0.5f + o, FogEvent.y(e) + 0.5f + o, 20, FogEvent.radius(e) + 0.3f);
}
public void copyFromCpu(){
staticFog.resize(world.width(), world.height());
staticFog.begin(Color.black);

View file

@ -100,7 +100,7 @@ public class MinimapRenderer{
updateUnitArray();
}else{
units.clear();
Groups.unit.each(units::add);
Groups.unit.copy(units);
}
float sz = baseSize * zoom;
@ -112,10 +112,12 @@ public class MinimapRenderer{
rect.set((dx - sz) * tilesize, (dy - sz) * tilesize, sz * 2 * tilesize, sz * 2 * tilesize);
for(Unit unit : units){
if(unit.inFogTo(player.team())) continue;
float rx = !withLabels ? (unit.x - rect.x) / rect.width * w : unit.x / (world.width() * tilesize) * w;
float ry = !withLabels ? (unit.y - rect.y) / rect.width * h : unit.y / (world.height() * tilesize) * h;
Draw.mixcol(unit.team().color, 1f);
Draw.mixcol(unit.team.color, 1f);
float scale = Scl.scl(1f) / 2f * scaling * 32f;
var region = unit.icon();
Draw.rect(region, x + rx, y + ry, scale, scale * (float)region.height / region.width, unit.rotation() - 90);
@ -144,11 +146,23 @@ public class MinimapRenderer{
zoom = z;
}
Draw.shader(Shaders.fog);
renderer.fog.getStaticTexture().setFilter(TextureFilter.nearest);
Texture staticTex = renderer.fog.getStaticTexture(), dynamicTex = renderer.fog.getDynamicTexture();
//crisp pixels
Tmp.tr1.set(renderer.fog.getStaticTexture());
staticTex.setFilter(TextureFilter.nearest);
dynamicTex.setFilter(TextureFilter.nearest);
Tmp.tr1.set(dynamicTex);
Tmp.tr1.set(region.u, 1f - region.v, region.u2, 1f - region.v2);
Draw.color(FogRenderer.dynamicColor);
Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h);
Tmp.tr1.texture = staticTex;
Draw.color(FogRenderer.staticColor);
Draw.rect(Tmp.tr1, x + w/2f, y + h/2f, w, h);
Draw.color();
Draw.shader();
}

View file

@ -6,6 +6,7 @@ import arc.struct.*;
import arc.util.*;
import mindustry.ai.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.entities.abilities.*;
import mindustry.game.*;
@ -191,7 +192,7 @@ public class SectorDamage{
//TODO would be nice if this worked in a more generic way, with two different calculations and paths
if(airOnly){
world.raycastEach(start.x, start.y, core.tileX(), core.tileY(), (x, y) -> {
World.raycastEach(start.x, start.y, core.tileX(), core.tileY(), (x, y) -> {
path.add(world.rawTile(x, y));
return false;
});

View file

@ -438,10 +438,6 @@ public class UnitType extends UnlockableContent{
lightRadius = Math.max(60f, hitSize * 2.3f);
}
if(fogRadius < 0){
fogRadius = lightRadius * 2f / 8f;
}
clipSize = Math.max(clipSize, lightRadius * 1.1f);
singleTarget = weapons.size <= 1 && !forceMultiTarget;
@ -469,6 +465,10 @@ public class UnitType extends UnlockableContent{
}
}
if(fogRadius < 0){
fogRadius = Math.max(lightRadius * 2.5f, 1f) / 8f;
}
if(weapons.isEmpty()){
range = maxRange = miningRange;
}
@ -787,6 +787,8 @@ public class UnitType extends UnlockableContent{
//region drawing
public void draw(Unit unit){
if(unit.inFogTo(Vars.player.team())) return;
Mechc mech = unit instanceof Mechc ? (Mechc)unit : null;
float z = unit.elevation > 0.5f ? (lowAltitude ? Layer.flyingUnitLow : Layer.flyingUnit) : groundLayer + Mathf.clamp(hitSize / 4000f, 0, 0.01f);

View file

@ -9,6 +9,7 @@ import arc.scene.ui.layout.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.entities.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
@ -125,7 +126,7 @@ public class RepairBeamWeapon extends Weapon{
if(targetBuildings){
//snap to closest building
Vars.world.raycastEachWorld(wx, wy, heal.lastEnd.x, heal.lastEnd.y, (x, y) -> {
World.raycastEachWorld(wx, wy, heal.lastEnd.x, heal.lastEnd.y, (x, y) -> {
var build = Vars.world.build(x, y);
if(build != null && build.team == unit.team && build.damaged()){
heal.target = build;

View file

@ -1,5 +1,6 @@
package mindustry.world.blocks.defense.turrets;
import arc.math.*;
import arc.struct.*;
import arc.util.*;
import mindustry.content.*;
@ -48,6 +49,7 @@ public class BaseTurret extends Block{
}
placeOverlapRange = Math.max(placeOverlapRange, range + placeOverlapMargin);
fogRadius = Math.max(Mathf.round(range / tilesize), fogRadius);
super.init();
}

View file

@ -352,7 +352,7 @@ public class PowerNode extends PowerBlock{
}
public static boolean insulated(int x, int y, int x2, int y2){
return world.raycast(x, y, x2, y2, (wx, wy) -> {
return World.raycast(x, y, x2, y2, (wx, wy) -> {
Building tile = world.build(wx, wy);
return tile != null && tile.isInsulated();
});

View file

@ -104,6 +104,7 @@ public class CoreBlock extends StorageBlock{
public void init(){
//assign to update clipSize internally
lightRadius = 30f + 20f * size;
fogRadius = Math.max(fogRadius, (int)(lightRadius / 8f * 2f));
emitLight = true;
super.init();