#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 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 = ""; imagePath = AssetPath(); 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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("{}?hueshift={}", 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()); 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 Plant::writeNetState(uint64_t fromVersion) { return m_netGroup.writeNetState(fromVersion); } void Plant::readNetState(ByteArray data, float interpolationTime) { m_netGroup.readNetState(std::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 Plant::spaces() const { return m_spaces; } List 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::pieces() const { return m_pieces; } RectF Plant::interactiveBoundBox() const { return RectF(m_boundBox); } void Plant::scanSpacesAndRoots() { auto imageMetadataDatabase = Root::singleton().imageMetadataDatabase(); // build spaces Set 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::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(float dt, uint64_t) { m_windTime += dt; 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, dt); } else { if (m_tileDamageStatus.damaged() && !m_tileDamageStatus.damageProtected()) { float damageEffectPercentage = m_tileDamageStatus.damageEffectPercentage(); m_windTime += damageEffectPercentage * 10 * dt; m_windLevel += damageEffectPercentage * 20; } m_netGroup.tickNetInterpolation(dt); } } 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.imagePath, 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(std::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("{}?hueshift={}", particle.string, hueshift); particle.image = particle.string; } particle.position = {m_tileDamageX + Random::randf(), m_tileDamageY + Random::randf()}; particle.translate(position()); renderCallback->addParticle(std::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(*assets->audio(sound.getString("file"))); audioInstance->setPosition(pos); audioInstance->setVolume(sound.getFloat("volume", 1.0f)); renderCallback->addAudio(std::move(audioInstance)); } } } } void Plant::readPieces(ByteArray pieces) { if (!pieces.empty()) { DataStreamBuffer ds(std::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(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(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() { for (auto& piece : m_pieces) piece.imagePath = piece.image; 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 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(); int segmentIdx = highest(); 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 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 spaces = piece.spaces; piece.spaces.clear(); for (auto const& space : spaces) { piece.spaces.add(space - breakPointI); } } Vec2F worldSpaceBreakPoint = breakPoint + Vec2F(tilePosition()); List segmentOrder; Map> 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(segment, worldSpaceBreakPoint, fallVector, description(), m_ceiling, m_stemDropConfig, m_foliageDropConfig, m_saplingDropConfig, first, random)); first = false; } m_piecesScanned = false; validatePieces(); } Vec2I Plant::baseDamagePosition(List 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; } }