Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarPlant.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarPlant.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarPlant.cpp')
-rw-r--r--source/game/StarPlant.cpp1072
1 files changed, 1072 insertions, 0 deletions
diff --git a/source/game/StarPlant.cpp b/source/game/StarPlant.cpp
new file mode 100644
index 0000000..b71643a
--- /dev/null
+++ b/source/game/StarPlant.cpp
@@ -0,0 +1,1072 @@
+#include "StarPlant.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarWorld.hpp"
+#include "StarRoot.hpp"
+#include "StarObjectDatabase.hpp"
+#include "StarPlantDrop.hpp"
+#include "StarImageMetadataDatabase.hpp"
+#include "StarAssets.hpp"
+#include "StarImage.hpp"
+#include "StarEntityRendering.hpp"
+#include "StarParticleDatabase.hpp"
+
+namespace Star {
+
+float const Plant::PlantScanThreshold = 0.1f;
+
+EnumMap<Plant::RotationType> const Plant::RotationTypeNames{
+ {Plant::RotationType::DontRotate, "dontRotate"},
+ {Plant::RotationType::RotateBranch, "rotateBranch"},
+ {Plant::RotationType::RotateLeaves, "rotateLeaves"},
+ {Plant::RotationType::RotateCrownBranch, "rotateCrownBranch"},
+ {Plant::RotationType::RotateCrownLeaves, "rotateCrownLeaves"},
+};
+
+Plant::PlantPiece::PlantPiece() {
+ image = "";
+ offset = {};
+ segmentIdx = 0;
+ structuralSegment = 0;
+ kind = PlantPieceKind::None;
+ zLevel = 0;
+ rotationType = RotationType::DontRotate;
+ rotationOffset = 0;
+ spaces = {};
+ flip = false;
+}
+
+Plant::Plant(TreeVariant const& config, uint64_t seed) : Plant() {
+ m_broken = false;
+ m_tilePosition = Vec2I();
+ m_windTime = 0.0f;
+ m_windLevel = 0.0f;
+ m_ceiling = config.ceiling;
+ m_piecesScanned = false;
+ m_fallsWhenDead = true;
+ m_piecesUpdated = true;
+ m_tileDamageEvent = false;
+
+ m_stemDropConfig = config.stemDropConfig;
+ m_foliageDropConfig = config.foliageDropConfig;
+ if (m_stemDropConfig.isNull())
+ m_stemDropConfig = JsonObject();
+ if (m_foliageDropConfig.isNull())
+ m_foliageDropConfig = JsonObject();
+
+ m_stemDropConfig = m_stemDropConfig.set("hueshift", config.stemHueShift);
+ m_foliageDropConfig = m_foliageDropConfig.set("hueshift", config.foliageHueShift);
+
+ JsonObject saplingDropConfig;
+ saplingDropConfig["stemName"] = config.stemName;
+ saplingDropConfig["stemHueShift"] = config.stemHueShift;
+ if (!m_foliageDropConfig.isNull()) {
+ saplingDropConfig["foliageName"] = config.foliageName;
+ saplingDropConfig["foliageHueShift"] = config.foliageHueShift;
+ }
+ m_saplingDropConfig = saplingDropConfig;
+
+ RandomSource rnd(seed);
+
+ float xOffset = 0;
+ float yOffset = 0;
+
+ float roffset = Random::randf() * 0.5f;
+
+ m_descriptions = config.descriptions;
+ m_ephemeral = config.ephemeral;
+ m_tileDamageParameters = config.tileDamageParameters;
+
+ int segment = 0;
+
+ auto assets = Root::singleton().assets();
+
+ // base
+ {
+ JsonObject bases = config.stemSettings.get("base").toObject();
+ String baseKey = bases.keys()[rnd.randInt(bases.size() - 1)];
+ JsonObject baseSettings = bases[baseKey].toObject();
+
+ JsonObject attachmentSettings = baseSettings["attachment"].toObject();
+
+ xOffset += attachmentSettings.get("bx").toDouble() / TilePixels;
+ yOffset += attachmentSettings.get("by").toDouble() / TilePixels;
+
+ String baseFile = AssetPath::relativeTo(config.stemDirectory, baseSettings.get("image").toString());
+ float baseImageHeight = assets->image(baseFile)->height();
+ if (config.ceiling)
+ yOffset = 1.0 - baseImageHeight / TilePixels;
+
+ {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", baseFile, config.stemHueShift);
+ piece.offset = Vec2F(xOffset, yOffset);
+ piece.segmentIdx = segment;
+ piece.structuralSegment = true;
+ piece.kind = PlantPieceKind::Stem;
+ piece.zLevel = 0.0f;
+ piece.rotationType = DontRotate;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+
+ // base leaves
+ JsonObject baseLeaves = config.foliageSettings.getObject("baseLeaves", {});
+ if (baseLeaves.contains(baseKey)) {
+ JsonObject baseLeavesSettings = baseLeaves.get(baseKey).toObject();
+ JsonObject attachmentSettings = baseLeavesSettings["attachment"].toObject();
+
+ float xOf = xOffset + attachmentSettings.get("bx").toDouble() / TilePixels;
+ float yOf = yOffset + attachmentSettings.get("by").toDouble() / TilePixels;
+
+ if (baseLeavesSettings.contains("image") && !baseLeavesSettings.get("image").toString().empty()) {
+ String baseLeavesFile =
+ AssetPath::relativeTo(config.foliageDirectory, baseLeavesSettings.get("image").toString());
+
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", baseLeavesFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = 3.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+
+ if (baseLeavesSettings.contains("backimage") && !baseLeavesSettings.get("backimage").toString().empty()) {
+ String baseLeavesBackFile =
+ AssetPath::relativeTo(config.foliageDirectory, baseLeavesSettings.get("backimage").toString());
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", baseLeavesBackFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = -1.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+ }
+
+ xOffset += attachmentSettings.get("x").toDouble() / TilePixels;
+ yOffset += attachmentSettings.get("y").toDouble() / TilePixels; // trunk height
+
+ segment++;
+ }
+
+ float branchYOffset = yOffset;
+
+ // trunk
+ {
+ JsonObject middles = config.stemSettings.get("middle").toObject();
+
+ int middleHeight = config.stemSettings.getInt("middleMinSize", 1) + rnd.randInt(config.stemSettings.getInt("middleMaxSize", 6) - config.stemSettings.getInt("middleMinSize", 1));
+
+ bool hasBranches = config.stemSettings.contains("branch");
+ JsonObject branches;
+ if (hasBranches) {
+ branches = config.stemSettings.get("branch").toObject();
+ if (branches.size() == 0)
+ hasBranches = false;
+ }
+
+ for (int i = 0; i < middleHeight; i++) {
+ String middleKey = middles.keys()[rnd.randInt(middles.size() - 1)];
+ JsonObject middleSettings = middles[middleKey].toObject();
+ JsonObject attachmentSettings = middleSettings["attachment"].toObject();
+
+ xOffset += attachmentSettings.get("bx").toDouble() / TilePixels;
+ yOffset += attachmentSettings.get("by").toDouble() / TilePixels;
+
+ String middleFile = AssetPath::relativeTo(config.stemDirectory, middleSettings.get("image").toString());
+
+ {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", middleFile, config.stemHueShift);
+ piece.offset = Vec2F(xOffset, yOffset);
+ piece.segmentIdx = segment;
+ piece.structuralSegment = true;
+ piece.kind = PlantPieceKind::Stem;
+ piece.zLevel = 1.0f;
+ piece.rotationType = DontRotate;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+
+ // trunk leaves
+ JsonObject trunkLeaves = config.foliageSettings.getObject("trunkLeaves", {});
+ if (trunkLeaves.contains(middleKey)) {
+ JsonObject trunkLeavesSettings = trunkLeaves.get(middleKey).toObject();
+ JsonObject attachmentSettings = trunkLeavesSettings["attachment"].toObject();
+
+ float xOf = xOffset + attachmentSettings.get("bx").toDouble() / TilePixels;
+ float yOf = yOffset + attachmentSettings.get("by").toDouble() / TilePixels;
+
+ if (trunkLeavesSettings.contains("image") && !trunkLeavesSettings.get("image").toString().empty()) {
+ String trunkLeavesFile =
+ AssetPath::relativeTo(config.foliageDirectory, trunkLeavesSettings.get("image").toString());
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", trunkLeavesFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = 3.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+
+ if (trunkLeavesSettings.contains("backimage") && !trunkLeavesSettings.get("backimage").toString().empty()) {
+ String trunkLeavesBackFile =
+ AssetPath::relativeTo(config.foliageDirectory, trunkLeavesSettings.get("backimage").toString());
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", trunkLeavesBackFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = -1.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = Random::randf() + roffset;
+ m_pieces.append(piece);
+ }
+ }
+
+ xOffset += attachmentSettings.get("x").toDouble() / TilePixels;
+ yOffset += attachmentSettings.get("y").toDouble() / TilePixels;
+
+ // branch
+ while (hasBranches && (yOffset >= branchYOffset) && ((middleHeight - i) > 0)) {
+ String branchKey = branches.keys()[rnd.randInt(branches.size() - 1)];
+ JsonObject branchSettings = branches[branchKey].toObject();
+ JsonObject attachmentSettings = branchSettings["attachment"].toObject();
+
+ float h = attachmentSettings.get("h").toDouble() / TilePixels;
+ if (yOffset < branchYOffset + (h / 2.0f))
+ break;
+
+ float xO = xOffset + attachmentSettings.get("bx").toDouble() / TilePixels;
+ float yO = branchYOffset + attachmentSettings.get("by").toDouble() / TilePixels;
+
+ if (config.stemSettings.getBool("alwaysBranch", false) || rnd.randInt(2 + i) != 0) {
+ float boffset = Random::randf() + roffset;
+ String branchFile = AssetPath::relativeTo(config.stemDirectory, branchSettings.get("image").toString());
+ {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", branchFile, config.stemHueShift);
+ piece.offset = Vec2F{xO, yO};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Stem;
+ piece.zLevel = 0.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateBranch;
+ piece.rotationOffset = boffset;
+ m_pieces.append(piece);
+ }
+ branchYOffset += h;
+
+ // branch leaves
+ JsonObject branchLeaves = config.foliageSettings.getObject("branchLeaves", {});
+ if (branchLeaves.contains(branchKey)) {
+ JsonObject branchLeavesSettings = branchLeaves.get(branchKey).toObject();
+ JsonObject attachmentSettings = branchLeavesSettings["attachment"].toObject();
+
+ float xOf = xO + attachmentSettings.get("bx").toDouble() / TilePixels;
+ float yOf = yO + attachmentSettings.get("by").toDouble() / TilePixels;
+
+ if (branchLeavesSettings.contains("image") && !branchLeavesSettings.get("image").toString().empty()) {
+ String branchLeavesFile =
+ AssetPath::relativeTo(config.foliageDirectory, branchLeavesSettings.get("image").toString());
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", branchLeavesFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = 3.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = boffset;
+ m_pieces.append(piece);
+ }
+
+ if (branchLeavesSettings.contains("backimage")
+ && !branchLeavesSettings.get("backimage").toString().empty()) {
+ String branchLeavesBackFile =
+ AssetPath::relativeTo(config.foliageDirectory, branchLeavesSettings.get("backimage").toString());
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", branchLeavesBackFile, config.foliageHueShift);
+ piece.offset = Vec2F{xOf, yOf};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = -1.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateLeaves;
+ piece.rotationOffset = boffset;
+ m_pieces.append(piece);
+ }
+ }
+
+ } else {
+ branchYOffset += (attachmentSettings.get("h").toDouble() / TilePixels) / (float)(1 + rnd.randInt(4));
+ }
+ }
+ segment++;
+ }
+ }
+
+ // crown
+ {
+ JsonObject crowns = config.stemSettings.getObject("crown", {});
+ bool hasCrown = crowns.size() > 0;
+ if (hasCrown) {
+ String crownKey = crowns.keys()[rnd.randInt(crowns.size() - 1)];
+ JsonObject crownSettings = crowns[crownKey].toObject();
+
+ JsonObject attachmentSettings = crownSettings["attachment"].toObject();
+
+ xOffset += attachmentSettings.get("bx").toDouble() / TilePixels;
+ yOffset += attachmentSettings.get("by").toDouble() / TilePixels;
+
+ float coffset = roffset + Random::randf();
+
+ String crownFile = AssetPath::relativeTo(config.stemDirectory, crownSettings.get("image").toString());
+ {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", crownFile, config.stemHueShift);
+ piece.offset = Vec2F{xOffset, yOffset};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Stem;
+ piece.zLevel = 0.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateCrownBranch;
+ piece.rotationOffset = coffset;
+ m_pieces.append(piece);
+ }
+ // crown leaves
+ JsonObject crownLeaves = config.foliageSettings.getObject("crownLeaves", {});
+ if (crownLeaves.contains(crownKey)) {
+ JsonObject crownLeavesSettings = crownLeaves.get(crownKey).toObject();
+
+ JsonObject attachmentSettings = crownLeavesSettings["attachment"].toObject();
+
+ float xO = xOffset + attachmentSettings.get("bx").toDouble() / TilePixels;
+ float yO = yOffset + attachmentSettings.get("by").toDouble() / TilePixels;
+
+ if (crownLeavesSettings.contains("image") && !crownLeavesSettings.get("image").toString().empty()) {
+ String crownLeavesFile =
+ AssetPath::relativeTo(config.foliageDirectory, crownLeavesSettings.get("image").toString());
+
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", crownLeavesFile, config.foliageHueShift);
+ piece.offset = Vec2F{xO, yO};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = 3.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateCrownLeaves;
+ piece.rotationOffset = coffset;
+ m_pieces.append(piece);
+ }
+
+ if (crownLeavesSettings.contains("backimage") && !crownLeavesSettings.get("backimage").toString().empty()) {
+ String crownLeavesBackFile =
+ AssetPath::relativeTo(config.foliageDirectory, crownLeavesSettings.get("backimage").toString());
+
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", crownLeavesBackFile, config.foliageHueShift);
+ piece.offset = Vec2F{xO, yO};
+ piece.segmentIdx = segment;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::Foliage;
+ piece.zLevel = -1.0f;
+ piece.rotationType = m_ceiling ? DontRotate : RotateCrownLeaves;
+ piece.rotationOffset = coffset;
+ m_pieces.append(piece);
+ }
+ }
+ }
+ }
+
+ sort(m_pieces, [](PlantPiece const& a, PlantPiece const& b) { return a.zLevel < b.zLevel; });
+ validatePieces();
+ setupNetStates();
+}
+
+Json Plant::diskStore() const {
+ return JsonObject{
+ {"tilePosition", jsonFromVec2I(m_tilePosition)},
+ {"ceiling", m_ceiling},
+ {"stemDropConfig", m_stemDropConfig},
+ {"foliageDropConfig", m_foliageDropConfig},
+ {"saplingDropConfig", m_saplingDropConfig},
+ {"descriptions", m_descriptions},
+ {"ephemeral", m_ephemeral},
+ {"tileDamageParameters", m_tileDamageParameters.toJson()},
+ {"fallsWhenDead", m_fallsWhenDead},
+ {"pieces", writePiecesToJson()},
+ };
+}
+
+ByteArray Plant::netStore() const {
+ DataStreamBuffer ds;
+ ds.viwrite(m_tilePosition[0]);
+ ds.viwrite(m_tilePosition[1]);
+ ds.write(m_ceiling);
+ ds.write(m_stemDropConfig);
+ ds.write(m_foliageDropConfig);
+ ds.write(m_saplingDropConfig);
+ ds.write(m_descriptions);
+ ds.write(m_ephemeral);
+ ds.write(m_tileDamageParameters);
+ ds.write(m_fallsWhenDead);
+ m_tileDamageStatus.netStore(ds);
+ ds.write(writePieces());
+
+ return ds.takeData();
+}
+
+Plant::Plant(GrassVariant const& config, uint64_t seed) : Plant() {
+ m_broken = false;
+ m_tilePosition = Vec2I();
+ m_ceiling = false;
+ m_windTime = 0.0f;
+ m_windLevel = 0.0f;
+ m_piecesScanned = false;
+ m_fallsWhenDead = false;
+ m_descriptions = config.descriptions;
+ m_ephemeral = config.ephemeral;
+ m_tileDamageParameters = config.tileDamageParameters;
+ m_piecesUpdated = true;
+
+ RandomSource rand(seed);
+
+ String imageName = AssetPath::relativeTo(config.directory, rand.randValueFrom(config.images));
+
+ Vec2F offset = Vec2F();
+ // If this is a ceiling plant, offset the image so that the [0, 0] space is
+ // at the top
+ if (config.ceiling) {
+ auto imgMetadata = Root::singleton().imageMetadataDatabase();
+ float imageHeight = imgMetadata->imageSize(imageName)[1];
+ offset = Vec2F(0.0f, 1.0f - imageHeight / TilePixels);
+ }
+
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", imageName, config.hueShift);
+ piece.offset = offset;
+ piece.segmentIdx = 0;
+ piece.structuralSegment = true;
+ piece.kind = PlantPieceKind::None;
+ m_pieces = {piece};
+
+ m_ceiling = config.ceiling;
+ validatePieces();
+ setupNetStates();
+}
+
+Plant::Plant(BushVariant const& config, uint64_t seed) : Plant() {
+ m_broken = false;
+ m_tilePosition = Vec2I();
+ m_ceiling = false;
+ m_windTime = 0.0f;
+ m_windLevel = 0.0f;
+ m_piecesScanned = false;
+ m_fallsWhenDead = false;
+ m_descriptions = config.descriptions;
+ m_ephemeral = config.ephemeral;
+ m_tileDamageParameters = config.tileDamageParameters;
+ m_piecesUpdated = true;
+
+ RandomSource rand(seed);
+ auto assets = Root::singleton().assets();
+
+ auto shape = rand.randValueFrom(config.shapes);
+ String shapeImageName = AssetPath::relativeTo(config.directory, shape.image);
+ float shapeImageHeight = assets->image(shapeImageName)->height();
+ Vec2F offset = Vec2F();
+ // If this is a ceiling plant, offset the image so that the [0, 0] space is
+ // at the top
+ if (config.ceiling)
+ offset = Vec2F(0.0f, 1.0f - shapeImageHeight / TilePixels);
+
+ {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", shapeImageName, config.baseHueShift);
+ piece.offset = offset;
+ piece.segmentIdx = 0;
+ piece.structuralSegment = true;
+ piece.kind = PlantPieceKind::None;
+ m_pieces.append(piece);
+ }
+
+ auto mod = rand.randValueFrom(shape.mods);
+ if (!mod.empty()) {
+ PlantPiece piece;
+ piece.image = strf("%s?hueshift=%s", AssetPath::relativeTo(config.directory, mod), config.modHueShift);
+ piece.offset = offset;
+ piece.segmentIdx = 0;
+ piece.structuralSegment = false;
+ piece.kind = PlantPieceKind::None;
+ m_pieces.append(piece);
+ }
+
+ m_ceiling = config.ceiling;
+ validatePieces();
+ setupNetStates();
+}
+
+Plant::Plant(Json const& diskStore) : Plant() {
+ m_tilePosition = jsonToVec2I(diskStore.get("tilePosition"));
+ m_ceiling = diskStore.getBool("ceiling");
+ m_stemDropConfig = diskStore.get("stemDropConfig");
+ m_foliageDropConfig = diskStore.get("foliageDropConfig");
+ m_saplingDropConfig = diskStore.get("saplingDropConfig");
+ m_descriptions = diskStore.get("descriptions");
+ m_ephemeral = diskStore.getBool("ephemeral");
+ m_tileDamageParameters = TileDamageParameters(diskStore.get("tileDamageParameters"));
+ m_fallsWhenDead = diskStore.getBool("fallsWhenDead");
+ readPiecesFromJson(diskStore.get("pieces"));
+
+ setupNetStates();
+}
+
+Plant::Plant(ByteArray const& netStore) : Plant() {
+ m_broken = false;
+ m_tilePosition = Vec2I();
+ m_ceiling = false;
+ m_windTime = 0.0f;
+ m_windLevel = 0.0f;
+ m_piecesScanned = false;
+ m_fallsWhenDead = false;
+ m_piecesUpdated = true;
+
+ DataStreamBuffer ds(netStore);
+ ds.viread(m_tilePosition[0]);
+ ds.viread(m_tilePosition[1]);
+ ds.read(m_ceiling);
+ ds.read(m_stemDropConfig);
+ ds.read(m_foliageDropConfig);
+ ds.read(m_saplingDropConfig);
+ ds.read(m_descriptions);
+ ds.read(m_ephemeral);
+ ds.read(m_tileDamageParameters);
+ ds.read(m_fallsWhenDead);
+ m_tileDamageStatus.netLoad(ds);
+ readPieces(ds.read<ByteArray>());
+
+ setupNetStates();
+}
+
+Plant::Plant() {
+ m_ephemeral = false;
+ m_piecesUpdated = true;
+ m_ceiling = false;
+ m_broken = false;
+ m_fallsWhenDead = false;
+ m_windTime = 0.0f;
+ m_windLevel = 0.0f;
+ m_piecesScanned = false;
+ m_tileDamageX = 0.0f;
+ m_tileDamageY = 0.0f;
+ m_tileDamageEventTrigger = false;
+ m_tileDamageEvent = false;
+}
+
+EntityType Plant::entityType() const {
+ return EntityType::Plant;
+}
+
+void Plant::init(World* world, EntityId entityId, EntityMode mode) {
+ Entity::init(world, entityId, mode);
+ validatePieces();
+ m_tilePosition = world->geometry().xwrap(m_tilePosition);
+}
+
+pair<ByteArray, uint64_t> Plant::writeNetState(uint64_t fromVersion) {
+ return m_netGroup.writeNetState(fromVersion);
+}
+
+void Plant::readNetState(ByteArray data, float interpolationTime) {
+ m_netGroup.readNetState(move(data), interpolationTime);
+}
+
+void Plant::enableInterpolation(float extrapolationHint) {
+ // Only enable plant interpolation when it actually matters, for things that
+ // generate PlantDrops so that they match when the PlantDrops appear.
+ if (m_fallsWhenDead)
+ m_netGroup.enableNetInterpolation(extrapolationHint);
+}
+
+void Plant::disableInterpolation() {
+ m_netGroup.disableNetInterpolation();
+}
+
+String Plant::description() const {
+ return m_descriptions.getString("description");
+}
+
+Vec2F Plant::position() const {
+ return Vec2F(m_tilePosition);
+}
+
+RectF Plant::metaBoundBox() const {
+ return m_metaBoundBox;
+}
+
+bool Plant::ephemeral() const {
+ return m_ephemeral;
+}
+
+Vec2I Plant::tilePosition() const {
+ return m_tilePosition;
+}
+
+void Plant::setTilePosition(Vec2I const& tilePosition) {
+ m_tilePosition = tilePosition;
+}
+
+List<Vec2I> Plant::spaces() const {
+ return m_spaces;
+}
+
+List<Vec2I> Plant::roots() const {
+ return m_roots;
+}
+
+Vec2I Plant::primaryRoot() const {
+ return m_ceiling ? Vec2I(0, 1) : Vec2I(0, -1);
+}
+
+bool Plant::ceiling() const {
+ return m_ceiling;
+}
+
+bool Plant::shouldDestroy() const {
+ return m_broken || m_pieces.size() == 0;
+}
+
+bool Plant::checkBroken() {
+ if (!m_broken) {
+ if (!allSpacesOccupied(m_roots)) {
+ if (m_fallsWhenDead) {
+ breakAtPosition(m_tilePosition, Vec2F(m_tilePosition));
+ return false;
+ } else
+ m_broken = true;
+ } else if (anySpacesOccupied(m_spaces))
+ m_broken = true;
+ }
+
+ return m_broken;
+}
+
+List<Plant::PlantPiece> Plant::pieces() const {
+ return m_pieces;
+}
+
+RectF Plant::interactiveBoundBox() const {
+ return RectF(m_boundBox);
+}
+
+void Plant::scanSpacesAndRoots() {
+ auto imageMetadataDatabase = Root::singleton().imageMetadataDatabase();
+
+ // build spaces
+ Set<Vec2I> spaces;
+
+ // always include the base position in spaces, it causes all kinds of problems if you don't
+ spaces.add({0, 0});
+
+ for (auto& piece : m_pieces) {
+ piece.imageSize = imageMetadataDatabase->imageSize(piece.image);
+ piece.spaces = Set<Vec2I>::from(
+ imageMetadataDatabase->imageSpaces(piece.image, piece.offset * TilePixels, PlantScanThreshold, piece.flip));
+ spaces.addAll(piece.spaces);
+ }
+
+ m_spaces = spaces.values();
+
+ m_boundBox = RectI::boundBoxOfPoints(m_spaces);
+
+ for (auto space : m_spaces) {
+ if (space[1] == 0) {
+ if (m_ceiling)
+ m_roots.push_back({space[0], 1});
+ else
+ m_roots.push_back({space[0], -1});
+ }
+ }
+}
+
+void Plant::calcBoundBox() {
+ RectF boundBox = RectF::boundBoxOfPoints(m_spaces);
+ // Plants are allowed to visibly occupy one outside space from the spaces
+ // they take up.
+ m_metaBoundBox = RectF(boundBox.min() - Vec2F(1, 1), boundBox.max() + Vec2F(2, 2));
+}
+
+float Plant::branchRotation(float xPos, float rotoffset) const {
+ if (!inWorld() || m_windLevel == 0.0f)
+ return 0.0f;
+
+ float intensity = fabs(m_windLevel);
+
+ return copysign(0.00117f, m_windLevel) * (std::sin(m_windTime + rotoffset + xPos / 10.0f) * intensity - intensity / 300.0f);
+}
+
+void Plant::update(uint64_t) {
+ m_windTime += WorldTimestep;
+ m_windTime = std::fmod(m_windTime, 628.32f);
+ m_windLevel = world()->windLevel(Vec2F(m_tilePosition));
+
+ if (isMaster()) {
+ if (m_tileDamageStatus.damaged())
+ m_tileDamageStatus.recover(m_tileDamageParameters, WorldTimestep);
+ } else {
+ if (m_tileDamageStatus.damaged() && !m_tileDamageStatus.damageProtected()) {
+ float damageEffectPercentage = m_tileDamageStatus.damageEffectPercentage();
+ m_windTime += damageEffectPercentage * 10 * WorldTimestep;
+ m_windLevel += damageEffectPercentage * 20;
+ }
+
+ m_netGroup.tickNetInterpolation(WorldTimestep);
+ }
+}
+
+void Plant::render(RenderCallback* renderCallback) {
+ float damageXOffset = Random::randf(-0.1f, 0.1f) * m_tileDamageStatus.damageEffectPercentage();
+
+ for (auto const& plantPiece : m_pieces) {
+ auto size = Vec2F(plantPiece.imageSize) / TilePixels;
+
+ Vec2F offset = plantPiece.offset;
+ if ((m_ceiling && offset[1] <= m_tileDamageY) || (!m_ceiling && offset[1] + size[1] >= m_tileDamageY))
+ offset[0] += damageXOffset;
+
+ auto drawable = Drawable::makeImage(plantPiece.image, 1.0f / TilePixels, false, offset);
+ if (plantPiece.flip)
+ drawable.scale(Vec2F(-1, 1));
+
+ if (plantPiece.rotationType == RotateCrownBranch || plantPiece.rotationType == RotateCrownLeaves) {
+ drawable.rotate(branchRotation(m_tilePosition[0], plantPiece.rotationOffset * 1.4f) * 0.7f, plantPiece.offset + Vec2F(size[0] / 2.0f, 0));
+ drawable.translate(Vec2F(0, -0.40f));
+ } else if (plantPiece.rotationType == RotateBranch || plantPiece.rotationType == RotateLeaves) {
+ drawable.rotate(branchRotation(m_tilePosition[0], plantPiece.rotationOffset * 1.4f), plantPiece.offset + Vec2F(size) / 2.0f);
+ }
+ drawable.translate(position());
+ renderCallback->addDrawable(move(drawable), RenderLayerPlant);
+ }
+
+ if (m_tileDamageEvent) {
+ m_tileDamageEvent = false;
+ if (m_stemDropConfig.type() == Json::Type::Object) {
+ Json particleConfig = m_stemDropConfig.get("particles", JsonObject()).get("damageTree", JsonObject());
+ JsonArray particleOptions = particleConfig.getArray("options", {});
+ auto hueshift = m_stemDropConfig.getFloat("hueshift", 0) / 360.0f;
+ auto density = particleConfig.getFloat("density", 1);
+ while (density-- > 0) {
+ auto config = Random::randValueFrom(particleOptions, {});
+ if (config.isNull() || config.size() == 0)
+ continue;
+ auto particle = Root::singleton().particleDatabase()->particle(config);
+ particle.color.hueShift(hueshift);
+ if (!particle.string.empty())
+ particle.string = strf("%s?hueshift=%s", particle.string, hueshift);
+ particle.position = {m_tileDamageX + Random::randf(), m_tileDamageY + Random::randf()};
+ particle.translate(position());
+ renderCallback->addParticle(move(particle));
+ }
+ JsonArray damageTreeSoundOptions = m_stemDropConfig.get("sounds", JsonObject()).getArray("damageTree", JsonArray());
+ if (damageTreeSoundOptions.size()) {
+ auto sound = Random::randFrom(damageTreeSoundOptions);
+ Vec2F pos = position() + Vec2F(m_tileDamageX + Random::randf(), m_tileDamageY + Random::randf());
+ auto assets = Root::singleton().assets();
+ auto audioInstance = make_shared<AudioInstance>(*assets->audio(sound.getString("file")));
+ audioInstance->setPosition(pos);
+ audioInstance->setVolume(sound.getFloat("volume", 1.0f));
+ renderCallback->addAudio(move(audioInstance));
+ }
+ }
+ }
+}
+
+void Plant::readPieces(ByteArray pieces) {
+ if (!pieces.empty()) {
+ DataStreamBuffer ds(move(pieces));
+ ds.readContainer(m_pieces, [](DataStream& ds, PlantPiece& piece) {
+ ds.read(piece.image);
+ ds.read(piece.offset[0]);
+ ds.read(piece.offset[1]);
+ ds.read(piece.rotationType);
+ ds.read(piece.rotationOffset);
+ ds.read(piece.structuralSegment);
+ ds.read(piece.kind);
+ ds.read(piece.segmentIdx);
+ ds.read(piece.flip);
+ });
+ m_piecesScanned = false;
+ if (inWorld())
+ validatePieces();
+ }
+}
+
+ByteArray Plant::writePieces() const {
+ return DataStreamBuffer::serializeContainer(m_pieces, [](DataStream& ds, PlantPiece const& piece) {
+ ds.write(piece.image);
+ ds.write(piece.offset[0]);
+ ds.write(piece.offset[1]);
+ ds.write(piece.rotationType);
+ ds.write(piece.rotationOffset);
+ ds.write(piece.structuralSegment);
+ ds.write(piece.kind);
+ ds.write(piece.segmentIdx);
+ ds.write(piece.flip);
+ });
+}
+
+void Plant::readPiecesFromJson(Json const& pieces) {
+ m_pieces = jsonToList<PlantPiece>(pieces, [](Json const& v) -> PlantPiece {
+ PlantPiece res;
+ res.image = v.getString("image");
+ res.offset = jsonToVec2F(v.get("offset"));
+ res.rotationType = RotationTypeNames.getLeft(v.getString("rotationType"));
+ res.rotationOffset = v.getFloat("rotationOffset");
+ res.structuralSegment = v.getBool("structuralSegment");
+ res.kind = (PlantPieceKind)v.getInt("kind");
+ res.segmentIdx = v.getInt("segmentIdx");
+ res.flip = v.getBool("flip");
+
+ return res;
+ });
+ m_piecesScanned = false;
+ if (inWorld())
+ validatePieces();
+}
+
+Json Plant::writePiecesToJson() const {
+ return jsonFromList<PlantPiece>(m_pieces, [](PlantPiece const& piece) -> Json {
+ return JsonObject{
+ {"image", piece.image},
+ {"offset", jsonFromVec2F(piece.offset)},
+ {"rotationType", RotationTypeNames.getRight(piece.rotationType)},
+ {"rotationOffset", piece.rotationOffset},
+ {"structuralSegment", piece.structuralSegment},
+ {"kind", piece.kind},
+ {"segmentIdx", piece.segmentIdx},
+ {"flip", piece.flip},
+ };
+ });
+}
+
+void Plant::validatePieces() {
+ if (!m_piecesScanned) {
+ scanSpacesAndRoots();
+ calcBoundBox();
+ m_piecesScanned = true;
+ }
+}
+
+void Plant::setupNetStates() {
+ m_netGroup.addNetElement(&m_tileDamageStatus);
+ m_netGroup.addNetElement(&m_piecesNetState);
+ m_netGroup.addNetElement(&m_tileDamageXNetState);
+ m_netGroup.addNetElement(&m_tileDamageYNetState);
+ m_netGroup.addNetElement(&m_tileDamageEventNetState);
+
+ m_netGroup.setNeedsStoreCallback(bind(&Plant::setNetStates, this));
+ m_netGroup.setNeedsLoadCallback(bind(&Plant::getNetStates, this));
+}
+
+void Plant::getNetStates() {
+ if (m_piecesNetState.pullUpdated()) {
+ readPieces(m_piecesNetState.get());
+ m_piecesUpdated = true;
+ }
+
+ m_tileDamageX = m_tileDamageXNetState.get();
+ m_tileDamageY = m_tileDamageYNetState.get();
+ if (m_tileDamageEventNetState.pullOccurred()) {
+ m_tileDamageEvent = true;
+ m_tileDamageEventTrigger = true;
+ }
+}
+
+void Plant::setNetStates() {
+ if (m_piecesUpdated) {
+ m_piecesNetState.set(writePieces());
+ m_piecesUpdated = false;
+ }
+ m_tileDamageXNetState.set(m_tileDamageX);
+ m_tileDamageYNetState.set(m_tileDamageY);
+ if (m_tileDamageEventTrigger) {
+ m_tileDamageEventTrigger = false;
+ m_tileDamageEventNetState.trigger();
+ }
+}
+
+bool Plant::damageTiles(List<Vec2I> const& positions, Vec2F const& sourcePosition, TileDamage const& tileDamage) {
+ auto position = baseDamagePosition(positions);
+
+ auto geometry = world()->geometry();
+
+ m_tileDamageStatus.damage(m_tileDamageParameters, tileDamage);
+ m_tileDamageX = geometry.diff(position[0], tilePosition()[0]);
+ m_tileDamageY = position[1] - tilePosition()[1];
+ m_tileDamageEvent = true;
+ m_tileDamageEventTrigger = true;
+
+ bool breaking = false;
+ if (m_tileDamageStatus.dead()) {
+ breaking = true;
+ if (m_fallsWhenDead) {
+ m_tileDamageStatus.reset();
+ breakAtPosition(position, sourcePosition);
+ } else {
+ m_broken = true;
+ }
+ }
+
+ return breaking;
+}
+
+void Plant::breakAtPosition(Vec2I const& position, Vec2F const& sourcePosition) {
+ auto geometry = world()->geometry();
+ Vec2I internalPos = geometry.diff(position, tilePosition());
+ size_t idx = highest<size_t>();
+ int segmentIdx = highest<int>();
+ for (size_t i = 0; i < m_pieces.size(); ++i) {
+ auto& piece = m_pieces[i];
+ if (piece.structuralSegment && piece.spaces.contains(internalPos)) {
+ if (piece.segmentIdx < segmentIdx) {
+ segmentIdx = piece.segmentIdx;
+ idx = i;
+ }
+ }
+ }
+
+ // default to highest structural piece
+ if (idx >= m_pieces.size()) {
+ for (size_t i = m_pieces.size(); i > 0; --i) {
+ auto& piece = m_pieces[i - 1];
+ if (piece.structuralSegment) {
+ segmentIdx = piece.segmentIdx;
+ idx = i - 1;
+ break;
+ }
+ }
+ }
+
+ // plant has no structural segments? this is a terrible fallback because it
+ // prevents destruction
+ if (idx >= m_pieces.size())
+ return;
+
+ PlantPiece breakPiece = m_pieces[idx];
+ Vec2F breakPoint = Vec2F(position) - Vec2F(tilePosition());
+ if (breakPiece.spaces.size()) {
+ RectF bounds = RectF::null();
+ for (auto space : breakPiece.spaces) {
+ bounds.combine(Vec2F(space));
+ bounds.combine(Vec2F(space) + Vec2F(1, 1));
+ }
+ breakPoint[0] = (bounds.max()[0] + bounds.min()[0]) / 2.0f;
+ if (!m_ceiling)
+ breakPoint[1] = bounds.min()[1];
+ else
+ breakPoint[1] = bounds.max()[1];
+ }
+
+ List<PlantPiece> droppedPieces;
+ if (m_pieces[idx].structuralSegment) {
+ idx = 0;
+ while (idx < m_pieces.size()) {
+ if (m_pieces[idx].segmentIdx >= segmentIdx) {
+ droppedPieces.append(m_pieces.takeAt(idx));
+ continue;
+ }
+ idx++;
+ }
+ } else {
+ droppedPieces.append(m_pieces.takeAt(idx));
+ }
+ m_piecesUpdated = true;
+
+ Vec2I breakPointI = Vec2I(round(breakPoint[0]), round(breakPoint[1]));
+
+ // Calculate a new origin for the droppedPieces
+ for (auto& piece : droppedPieces) {
+ piece.offset -= breakPoint;
+ Set<Vec2I> spaces = piece.spaces;
+ piece.spaces.clear();
+ for (auto const& space : spaces) {
+ piece.spaces.add(space - breakPointI);
+ }
+ }
+
+ Vec2F worldSpaceBreakPoint = breakPoint + Vec2F(tilePosition());
+
+ List<int> segmentOrder;
+ Map<int, List<PlantPiece>> segments;
+ for (auto& piece : droppedPieces) {
+ if (!segments.contains(piece.segmentIdx))
+ segmentOrder.append(piece.segmentIdx);
+ segments[piece.segmentIdx].append(piece);
+ }
+ reverse(segmentOrder);
+ float random = Random::randf(-0.3f, 0.3f);
+ auto fallVector = (worldSpaceBreakPoint - sourcePosition).normalized();
+ bool first = true;
+ for (auto segmentIdx : segmentOrder) {
+ auto segment = segments[segmentIdx];
+ world()->addEntity(make_shared<PlantDrop>(segment,
+ worldSpaceBreakPoint,
+ fallVector,
+ description(),
+ m_ceiling,
+ m_stemDropConfig,
+ m_foliageDropConfig,
+ m_saplingDropConfig,
+ first,
+ random));
+ first = false;
+ }
+
+ m_piecesScanned = false;
+
+ validatePieces();
+}
+
+Vec2I Plant::baseDamagePosition(List<Vec2I> const& positions) const {
+ starAssert(positions.size());
+ auto res = positions.at(0);
+
+ for (auto const& piece : m_pieces) {
+ if (piece.structuralSegment) {
+ for (auto space : piece.spaces) {
+ for (auto position : positions) {
+ if (world()->geometry().equal(m_tilePosition + space, position)) {
+ // if this space is a "better match" for the root of the plant
+ if ((res[1] < position[1]) == m_ceiling) {
+ res = position;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return res;
+}
+
+bool Plant::damagable() const {
+ if (m_stemDropConfig.type() != Json::Type::Object)
+ return true;
+ if (!m_stemDropConfig.getBool("destructable", true))
+ return false;
+ return true;
+}
+
+}