using System; using ai.behaviours; using HarmonyLib; using UnityEngine; namespace IdleSpectator; /// Building damage/destroy/eat/complete interest patches from mutation discovery. [HarmonyPatch] public static class BuildingEventPatches { [HarmonyPatch(typeof(BehDealDamageToTargetBuilding), nameof(BehDealDamageToTargetBuilding.execute))] [HarmonyPostfix] public static void PostfixDamageBuilding(Actor pActor, BehResult __result) { // Routine siege chips are Layer B noise. Camera waits for consume. _ = pActor; _ = __result; } [HarmonyPatch(typeof(BehConsumeTargetBuilding), nameof(BehConsumeTargetBuilding.execute))] [HarmonyPostfix] public static void PostfixConsumeBuilding(Actor pActor, BehResult __result) { if (pActor == null || !pActor.isAlive() || __result == BehResult.Stop) { return; } Building food = null; try { food = pActor.beh_building_target; } catch { food = null; } string label = EventReason.BuildingEat(pActor, food); bool fruitForage = IsFruitForage(food); // Fruit bushes are high-rate ambient - fill-band strength so they do not crowd combat/story. // Real settlement eats (granary/etc.) stay camera-competitive. float strength = fruitForage ? 42f : 78f; string category = fruitForage ? "Forage" : "Spectacle"; string assetId = fruitForage ? "building_consume_fruit" : "building_consume"; Emit(assetId, strength, category, pActor, label); } [HarmonyPatch(typeof(Building), "startDestroyBuilding")] [HarmonyPostfix] public static void PostfixStartDestroy(Building __instance) { // Ambient object/building falls are Layer B only. // Nearest-unit attach + "A Tree falls near X" was winning Watching then blanking via identity_filter. // Intentional spectacle remains building_consume (actor-led eat). _ = __instance; } /// /// Civ construction finished (houses, temples, statues). /// Nature/tree completes stay silent - not settlement spectacle. /// [HarmonyPatch(typeof(Building), nameof(Building.completeConstruction))] [HarmonyPostfix] public static void PostfixCompleteConstruction(Building __instance) { if (!AgentHarness.LiveFeedsAllowed || __instance?.asset == null) { return; } if (!LibraryAssetNames.IsCivBuilding(__instance.asset)) { return; } string buildingId = ""; try { buildingId = __instance.asset.id ?? ""; } catch { buildingId = ""; } bool spectacle = LibraryAssetNames.IsSpectacleBuilding(buildingId, __instance.asset); float strength = spectacle ? 86f : 72f; Vector3 pos = BuildingWorldPos(__instance); Actor follow = WorldActivityScanner.FindNearestAliveUnit(pos, 48f); string label = EventReason.BuildingComplete(follow, buildingId, spectacle); string key = "building:complete:" + (string.IsNullOrEmpty(buildingId) ? "unknown" : buildingId) + ":" + EventFeedUtil.SafeId(follow); EventFeedUtil.Register( key, "building", "Settlement", label, string.IsNullOrEmpty(buildingId) ? "building_complete" : buildingId, strength, pos, follow, locationOnly: follow == null, minWatch: 6f, maxWatch: spectacle ? 28f : 18f); } private static bool IsFruitForage(Building food) { if (food?.asset == null) { return false; } try { string type = food.asset.type ?? ""; return string.Equals(type, "type_fruits", StringComparison.OrdinalIgnoreCase); } catch { return false; } } private static void Emit(string eventId, float strength, string category, Actor actor, string label) { if (!AgentHarness.LiveFeedsAllowed) { return; } string key = "building:" + eventId + ":" + EventFeedUtil.SafeId(actor); EventFeedUtil.Register( key, "building", string.IsNullOrEmpty(category) ? "Spectacle" : category, label, eventId, strength, actor.current_position, actor); } private static Vector3 BuildingWorldPos(Building building) { try { if (building.current_tile != null) { return building.current_tile.posV3; } } catch { // fall through } try { return building.current_position; } catch { return Vector3.zero; } } }