Generate tool bugfixes / Map edits / New blocks
BIN
core/assets-raw/sprites/blocks/environment/sporerocks-large.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
core/assets-raw/sprites/blocks/environment/sporerocks1.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
core/assets-raw/sprites/blocks/environment/sporerocks2.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
@ -250,6 +250,7 @@ filter.rivernoise = River Noise
|
||||
filter.scatter = Scatter
|
||||
filter.terrain = Terrain
|
||||
filter.option.scale = Scale
|
||||
filter.option.chance = Chance
|
||||
filter.option.mag = Magnitude
|
||||
filter.option.threshold = Threshold
|
||||
filter.option.circle-scale = Circle Scale
|
||||
@ -258,6 +259,8 @@ filter.option.falloff = Falloff
|
||||
filter.option.floor = Floor
|
||||
filter.option.wall = Wall
|
||||
filter.option.ore = Ore
|
||||
filter.option.floor2 = Secondary Floor
|
||||
filter.option.threshold2 = Secondary Threshold
|
||||
width = Width:
|
||||
height = Height:
|
||||
menu = Menu
|
||||
@ -340,6 +343,7 @@ blocks.poweroutput = Power Output: {0}
|
||||
blocks.powercapacity = Power Capacity
|
||||
blocks.powershot = Power/Shot
|
||||
blocks.targetsair = Targets Air
|
||||
blocks.targetsground = Targets Ground
|
||||
blocks.items = Items: {0}
|
||||
blocks.itemsmoved = Move Speed
|
||||
blocks.shootrange = Range
|
||||
@ -563,6 +567,8 @@ block.grass.name = Grass
|
||||
block.salt.name = Salt
|
||||
block.sandrocks.name = Sand Rocks
|
||||
block.spore-pine.name = Spore Pine
|
||||
block.sporerocks.name = Spore Rocks
|
||||
block.rock.name = Rock
|
||||
block.shale.name = Shale
|
||||
block.shale-boulder.name = Shale Boulder
|
||||
block.moss.name = Moss
|
||||
|
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 500 B |
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 195 KiB |
@ -66,6 +66,8 @@ public class Vars{
|
||||
public static final float finalWorldBounds = worldBounds + 500;
|
||||
/**ticks spent out of bound until self destruct.*/
|
||||
public static final float boundsCountdown = 60*7;
|
||||
/**for map generator dialog*/
|
||||
public static boolean updateEditorOnChange = false;
|
||||
/**size of tiles in units*/
|
||||
public static final int tilesize = 8;
|
||||
/**all choosable player colors in join/host dialog*/
|
||||
|
@ -36,9 +36,9 @@ public class Blocks implements ContentList{
|
||||
|
||||
//environment
|
||||
air, part, spawn, deepwater, water, tar, stone, craters, charr, sand, ice, snow,
|
||||
holostone, rocks, icerocks, cliffs, sporePine, pine, whiteTree, whiteTreeDead, sporeCluster,
|
||||
holostone, rocks, sporerocks, icerocks, cliffs, sporePine, pine, whiteTree, whiteTreeDead, sporeCluster,
|
||||
iceSnow, sandWater, duneRocks, sandRocks, moss, sporeMoss, shale, shaleRocks, shaleBoulder, grass, salt,
|
||||
metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor5, ignarock, magmarock, hotrock, snowrocks,
|
||||
metalFloor, metalFloorDamaged, metalFloor2, metalFloor3, metalFloor5, ignarock, magmarock, hotrock, snowrocks, rock,
|
||||
|
||||
//ores
|
||||
oreCopper, oreLead, oreScrap, oreCoal, oreTitanium, oreThorium,
|
||||
@ -214,6 +214,14 @@ public class Blocks implements ContentList{
|
||||
variants = 2;
|
||||
}};
|
||||
|
||||
sporerocks = new StaticWall("sporerocks"){{
|
||||
variants = 2;
|
||||
}};
|
||||
|
||||
rock = new Rock("rock"){{
|
||||
variants = 2;
|
||||
}};
|
||||
|
||||
icerocks = new StaticWall("icerocks"){{
|
||||
variants = 2;
|
||||
}};
|
||||
@ -694,7 +702,7 @@ public class Blocks implements ContentList{
|
||||
}};
|
||||
|
||||
junction = new Junction("junction"){{
|
||||
requirements(Category.distribution, ItemStack.with(Items.copper, 3));
|
||||
requirements(Category.distribution, ItemStack.with(Items.copper, 3), true);
|
||||
speed = 26;
|
||||
capacity = 32;
|
||||
health = 25;
|
||||
@ -1067,20 +1075,21 @@ public class Blocks implements ContentList{
|
||||
}};
|
||||
|
||||
scatter = new BurstTurret("scatter"){{
|
||||
requirements(Category.turret, ItemStack.with(Items.copper, 170, Items.lead, 90), true);
|
||||
requirements(Category.turret, ItemStack.with(Items.copper, 170, Items.lead, 90));
|
||||
ammo(
|
||||
Items.scrap, Bullets.flakScrap,
|
||||
Items.lead, Bullets.flakLead
|
||||
);
|
||||
reload = 45f;
|
||||
reload = 43f;
|
||||
range = 160f;
|
||||
size = 2;
|
||||
burstSpacing = 5f;
|
||||
shots = 2;
|
||||
targetGround = false;
|
||||
|
||||
recoil = 2f;
|
||||
rotatespeed = 10f;
|
||||
inaccuracy = 18f;
|
||||
inaccuracy = 17f;
|
||||
shootCone = 35f;
|
||||
|
||||
health = 220*size*size;
|
||||
|
@ -6,10 +6,12 @@ import io.anuke.arc.function.Supplier;
|
||||
import io.anuke.arc.graphics.Pixmap;
|
||||
import io.anuke.arc.graphics.Pixmap.Format;
|
||||
import io.anuke.arc.graphics.Texture;
|
||||
import io.anuke.arc.scene.ui.Image;
|
||||
import io.anuke.arc.scene.ui.layout.Stack;
|
||||
import io.anuke.arc.scene.ui.layout.Table;
|
||||
import io.anuke.arc.util.Scaling;
|
||||
import io.anuke.arc.util.async.AsyncExecutor;
|
||||
import io.anuke.arc.util.async.AsyncResult;
|
||||
import io.anuke.mindustry.content.Blocks;
|
||||
import io.anuke.mindustry.editor.generation.*;
|
||||
import io.anuke.mindustry.editor.generation.GenerateFilter.GenerateInput;
|
||||
import io.anuke.mindustry.game.Team;
|
||||
@ -21,7 +23,7 @@ import io.anuke.mindustry.world.Block;
|
||||
import io.anuke.mindustry.world.Tile;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.mindustry.Vars.mobile;
|
||||
import static io.anuke.mindustry.Vars.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class MapGenerateDialog extends FloatingDialog{
|
||||
@ -48,8 +50,10 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
shown(this::setup);
|
||||
addCloseButton();
|
||||
buttons.addButton("$editor.apply", () -> {
|
||||
apply();
|
||||
hide();
|
||||
ui.loadAnd(() -> {
|
||||
apply();
|
||||
hide();
|
||||
});
|
||||
}).size(160f, 64f);
|
||||
buttons.addButton("$editor.randomize", () -> {
|
||||
for(GenerateFilter filter : filters){
|
||||
@ -75,15 +79,20 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
cont.clear();
|
||||
cont.table("flat", t -> {
|
||||
t.margin(8f);
|
||||
t.add(new BorderImage(texture)).size(400f).padRight(6);
|
||||
t.stack(new BorderImage(texture), new Stack(){{
|
||||
add(new Image("loadDim"));
|
||||
add(new Image("icon-refresh"){{
|
||||
setScaling(Scaling.none);
|
||||
}});
|
||||
visible(() -> generating);
|
||||
}}).size(mobile ? 300f : 400f).padRight(6);
|
||||
t.pane(p -> filterTable = p).width(300f).get().setScrollingDisabled(true, false);
|
||||
}).grow();
|
||||
|
||||
update();
|
||||
|
||||
buffer1 = create();
|
||||
buffer2 = create();
|
||||
|
||||
update();
|
||||
rebuildFilters();
|
||||
}
|
||||
|
||||
@ -186,6 +195,16 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
result.get();
|
||||
}
|
||||
|
||||
buffer1 = null;
|
||||
buffer2 = null;
|
||||
generating = false;
|
||||
if(pixmap != null){
|
||||
pixmap.dispose();
|
||||
texture.dispose();
|
||||
pixmap = null;
|
||||
texture = null;
|
||||
}
|
||||
|
||||
//writeback buffer
|
||||
DummyTile[][] writeTiles = new DummyTile[editor.width()][editor.height()];
|
||||
|
||||
@ -196,7 +215,7 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : filters){
|
||||
input.setFilter(filter, (x, y) -> dset(editor.tile(x, y)));
|
||||
input.setFilter(filter, editor.width(), editor.height(), 1, (x, y) -> dset(editor.tile(x, y)));
|
||||
//write to buffer
|
||||
for(int x = 0; x < editor.width(); x++){
|
||||
for(int y = 0; y < editor.height(); y++){
|
||||
@ -215,10 +234,10 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
DummyTile write = writeTiles[x][y];
|
||||
|
||||
tile.setRotation((byte)write.rotation);
|
||||
tile.setFloor((Floor)write.floor);
|
||||
tile.setBlock(write.block);
|
||||
tile.setTeam(write.team);
|
||||
tile.setOre(write.ore);
|
||||
tile.setFloor((Floor)content.block(write.floor));
|
||||
tile.setBlock(content.block(write.block));
|
||||
tile.setTeam(Team.all[write.team]);
|
||||
tile.setOre(content.block(write.ore));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -239,74 +258,81 @@ public class MapGenerateDialog extends FloatingDialog{
|
||||
Array<GenerateFilter> copy = new Array<>(filters);
|
||||
|
||||
result = executor.submit(() -> {
|
||||
generating = true;
|
||||
try{
|
||||
generating = true;
|
||||
|
||||
if(!filters.isEmpty()){
|
||||
//write to buffer1 for reading
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : copy){
|
||||
input.setFilter(filter, pixmap.getWidth(), pixmap.getHeight(), scaling, (x, y) -> buffer1[x][y]);
|
||||
//read from buffer1 and write to buffer2
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
int x = px * scaling, y = py * scaling;
|
||||
DummyTile tile = buffer1[px][py];
|
||||
input.begin(editor, x, y, content.block(tile.floor), content.block(tile.block), content.block(tile.ore));
|
||||
filter.apply(input);
|
||||
buffer2[px][py].set(input.floor, input.block, input.ore, Team.all[tile.team], tile.rotation);
|
||||
}
|
||||
}
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
buffer1[px][py].set(buffer2[px][py]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!filters.isEmpty()){
|
||||
//write to buffer1 for reading
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
buffer1[px][py].set(editor.tile(px * scaling, py * scaling));
|
||||
int color;
|
||||
//get result from buffer1 if there's filters left, otherwise get from editor directly
|
||||
if(filters.isEmpty()){
|
||||
Tile tile = editor.tile(px * scaling, py * scaling);
|
||||
color = MapIO.colorFor(tile.floor(), tile.block(), tile.ore(), Team.none);
|
||||
}else{
|
||||
DummyTile tile = buffer1[px][py];
|
||||
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.none);
|
||||
}
|
||||
pixmap.drawPixel(px, pixmap.getHeight() - 1 - py, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(GenerateFilter filter : copy){
|
||||
input.setFilter(filter, (x, y) -> buffer1[x][y]);
|
||||
//read from buffer1 and write to buffer2
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
int x = px*scaling, y = py*scaling;
|
||||
DummyTile tile = buffer1[px][py];
|
||||
input.begin(editor, x, y, tile.floor, tile.block, tile.ore);
|
||||
filter.apply(input);
|
||||
buffer2[px][py].set(input.floor, input.block, input.ore, tile.team, tile.rotation);
|
||||
}
|
||||
}
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
buffer1[px][py].set(buffer2[px][py]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int px = 0; px < pixmap.getWidth(); px++){
|
||||
for(int py = 0; py < pixmap.getHeight(); py++){
|
||||
int color;
|
||||
//get result from buffer1 if there's filters left, otherwise get from editor directly
|
||||
if(filters.isEmpty()){
|
||||
Tile tile = editor.tile(px * scaling, py * scaling);
|
||||
color = MapIO.colorFor(tile.floor(), tile.block(), tile.ore(), Team.none);
|
||||
}else{
|
||||
DummyTile tile = buffer1[px][py];
|
||||
color = MapIO.colorFor(tile.floor, tile.block, tile.ore, Team.none);
|
||||
}
|
||||
pixmap.drawPixel(px, pixmap.getHeight() - 1 - py, color);
|
||||
}
|
||||
}
|
||||
|
||||
Core.app.post(() -> {
|
||||
texture.draw(pixmap, 0, 0);
|
||||
Core.app.post(() -> {
|
||||
texture.draw(pixmap, 0, 0);
|
||||
generating = false;
|
||||
});
|
||||
}catch(Exception e){
|
||||
generating = false;
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static class DummyTile{
|
||||
public Block block = Blocks.air, ore = Blocks.air, floor = Blocks.air;
|
||||
public Team team = Team.none;
|
||||
public int rotation;
|
||||
public byte block, floor, ore, team, rotation;
|
||||
|
||||
void set(Block floor, Block wall, Block ore, Team team, int rotation){
|
||||
this.floor = floor;
|
||||
this.block = wall;
|
||||
this.ore = ore;
|
||||
this.team = team;
|
||||
this.rotation = rotation;
|
||||
this.floor = floor.id;
|
||||
this.block = wall.id;
|
||||
this.ore = ore.id;
|
||||
this.team = (byte)team.ordinal();
|
||||
this.rotation = (byte)rotation;
|
||||
}
|
||||
|
||||
void set(DummyTile other){
|
||||
set(other.floor, other.block, other.ore, other.team, other.rotation);
|
||||
this.floor = other.floor;
|
||||
this.block = other.block;
|
||||
this.ore = other.ore;
|
||||
this.team = other.team;
|
||||
this.rotation = other.rotation;
|
||||
}
|
||||
|
||||
void set(Tile other){
|
||||
|
@ -267,7 +267,9 @@ public class MapView extends Element implements GestureListener{
|
||||
Draw.color(Pal.remove);
|
||||
Lines.stroke(2f);
|
||||
Lines.rect(centerx - sclwidth / 2 - 1, centery - sclheight / 2 - 1, sclwidth + 2, sclheight + 2);
|
||||
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
if(Core.scene.getKeyboardFocus() != null && isDescendantOf(Core.scene.getKeyboardFocus())){
|
||||
editor.renderer().draw(centerx - sclwidth / 2, centery - sclheight / 2, sclwidth, sclheight);
|
||||
}
|
||||
Draw.reset();
|
||||
|
||||
if(!ScissorStack.pushScissors(rect.set(x, y, width, height))){
|
||||
|
@ -4,6 +4,8 @@ import io.anuke.mindustry.editor.MapGenerateDialog.DummyTile;
|
||||
import io.anuke.mindustry.editor.generation.FilterOption.SliderOption;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
|
||||
import static io.anuke.mindustry.Vars.content;
|
||||
|
||||
public class DistortFilter extends GenerateFilter{
|
||||
float scl = 40, mag = 5;
|
||||
|
||||
@ -16,10 +18,10 @@ public class DistortFilter extends GenerateFilter{
|
||||
|
||||
@Override
|
||||
public void apply(){
|
||||
DummyTile tile = in.tile(in.x + noise(in.x, in.y, scl, mag)-mag/2f, in.y + noise(in.x, in.y+o, scl, mag)-mag/2f);
|
||||
DummyTile tile = in.tile(in.x / (in.scaling) + (noise(in.x, in.y, scl, mag)-mag/2f)/in.scaling, in.y / (in.scaling) + (noise(in.x, in.y+o, scl, mag)-mag/2f)/in.scaling);
|
||||
|
||||
in.floor = tile.floor;
|
||||
if(!tile.block.synthetic() && !in.block.synthetic()) in.block = tile.block;
|
||||
if(!((Floor)in.floor).isLiquid) in.ore = tile.ore;
|
||||
in.floor = content.block(tile.floor);
|
||||
if(!content.block(tile.block).synthetic() && !in.block.synthetic()) in.block = content.block(tile.block);
|
||||
if(!((Floor)in.floor).isLiquid) in.ore = content.block(tile.ore);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import io.anuke.mindustry.world.Block.Icon;
|
||||
import io.anuke.mindustry.world.blocks.Floor;
|
||||
import io.anuke.mindustry.world.blocks.OreBlock;
|
||||
|
||||
import static io.anuke.mindustry.Vars.updateEditorOnChange;
|
||||
|
||||
public abstract class FilterOption{
|
||||
public static final Predicate<Block> floorsOnly = b -> (b instanceof Floor && !(b instanceof OreBlock)) && Core.atlas.isFound(b.icon(Icon.full));
|
||||
public static final Predicate<Block> wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full));
|
||||
@ -40,7 +42,11 @@ public abstract class FilterOption{
|
||||
table.row();
|
||||
Slider slider = table.addSlider(min, max, (max-min)/200f, setter).growX().get();
|
||||
slider.setValue(getter.get());
|
||||
slider.changed(changed);
|
||||
if(updateEditorOnChange){
|
||||
slider.changed(changed);
|
||||
}else{
|
||||
slider.released(changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ public abstract class GenerateFilter{
|
||||
public Floor srcfloor;
|
||||
public Block srcblock;
|
||||
public Block srcore;
|
||||
public int x, y;
|
||||
public int x, y, width, height, scaling;
|
||||
|
||||
public MapEditor editor;
|
||||
public Block floor, block, ore;
|
||||
@ -74,14 +74,17 @@ public abstract class GenerateFilter{
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public void setFilter(GenerateFilter filter, TileProvider buffer){
|
||||
public void setFilter(GenerateFilter filter, int width, int height, int scaling, TileProvider buffer){
|
||||
this.buffer = buffer;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.scaling = scaling;
|
||||
noise.setSeed(filter.seed);
|
||||
pnoise.setSeed((int)(filter.seed + 1));
|
||||
}
|
||||
|
||||
DummyTile tile(float x, float y){
|
||||
return buffer.get(Mathf.clamp((int)x, 0, editor.width() - 1), Mathf.clamp((int)y, 0, editor.height() - 1));
|
||||
return buffer.get(Mathf.clamp((int)x, 0, width - 1), Mathf.clamp((int)y, 0, height - 1));
|
||||
}
|
||||
|
||||
public interface TileProvider{
|
||||
|
@ -32,20 +32,18 @@ public abstract class FlyingUnit extends BaseUnit{
|
||||
target = null;
|
||||
}
|
||||
|
||||
if(target == null){
|
||||
retarget(() -> {
|
||||
targetClosest();
|
||||
|
||||
retarget(() -> {
|
||||
targetClosest();
|
||||
if(target == null) targetClosestEnemyFlag(BlockFlag.producer);
|
||||
if(target == null) targetClosestEnemyFlag(BlockFlag.turret);
|
||||
|
||||
if(target == null) targetClosestEnemyFlag(BlockFlag.producer);
|
||||
if(target == null) targetClosestEnemyFlag(BlockFlag.turret);
|
||||
if(target == null){
|
||||
setState(patrol);
|
||||
}
|
||||
});
|
||||
|
||||
if(target == null){
|
||||
setState(patrol);
|
||||
}
|
||||
});
|
||||
|
||||
}else{
|
||||
if(target != null){
|
||||
attack(type.attackLength);
|
||||
|
||||
if((Angles.near(angleTo(target), rotation, type.shootCone) || getWeapon().ignoreRotation) //bombers and such don't care about rotation
|
||||
|
@ -58,6 +58,7 @@ public abstract class Turret extends Block{
|
||||
protected float shootShake = 0f;
|
||||
protected float xRand = 0f;
|
||||
protected boolean targetAir = true;
|
||||
protected boolean targetGround = true;
|
||||
|
||||
protected Vector2 tr = new Vector2();
|
||||
protected Vector2 tr2 = new Vector2();
|
||||
@ -108,6 +109,7 @@ public abstract class Turret extends Block{
|
||||
stats.add(BlockStat.reload, 60f / reload, StatUnit.none);
|
||||
stats.add(BlockStat.shots, shots, StatUnit.none);
|
||||
stats.add(BlockStat.targetsAir, targetAir);
|
||||
stats.add(BlockStat.targetsGround, targetGround);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -201,7 +203,7 @@ public abstract class Turret extends Block{
|
||||
TurretEntity entity = tile.entity();
|
||||
|
||||
entity.target = Units.getClosestTarget(tile.getTeam(),
|
||||
tile.drawx(), tile.drawy(), range, e -> !e.isDead() && (!e.isFlying() || targetAir));
|
||||
tile.drawx(), tile.drawy(), range, e -> !e.isDead() && (!e.isFlying() || targetAir) && (e.isFlying() || targetGround));
|
||||
}
|
||||
|
||||
protected void turnToTarget(Tile tile, float targetRot){
|
||||
|
@ -47,6 +47,7 @@ public enum BlockStat{
|
||||
reload(StatCategory.shooting),
|
||||
powerShot(StatCategory.shooting),
|
||||
targetsAir(StatCategory.shooting),
|
||||
targetsGround(StatCategory.shooting),
|
||||
|
||||
boostItem(StatCategory.optional),
|
||||
boostLiquid(StatCategory.optional),;
|
||||
|