整理scoure
This commit is contained in:
357
Source/WulaFallenEmpire/ThingComp/CompApparelInterceptor.cs
Normal file
357
Source/WulaFallenEmpire/ThingComp/CompApparelInterceptor.cs
Normal file
@@ -0,0 +1,357 @@
|
||||
using HarmonyLib;
|
||||
using RimWorld;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_ApparelInterceptor : CompProperties
|
||||
{
|
||||
public float radius = 3f;
|
||||
public int startupDelay = 0;
|
||||
public int rechargeDelay = 3200;
|
||||
public int hitPoints = 100;
|
||||
|
||||
public bool interceptGroundProjectiles = false;
|
||||
public bool interceptNonHostileProjectiles = false;
|
||||
public bool interceptAirProjectiles = true;
|
||||
|
||||
public EffecterDef soundInterceptEffecter;
|
||||
public EffecterDef soundBreakEffecter;
|
||||
public EffecterDef reactivateEffect;
|
||||
|
||||
public Color color = new Color(0.5f, 0.5f, 0.9f);
|
||||
public bool drawWithNoSelection = true;
|
||||
public bool isImmuneToEMP = false;
|
||||
|
||||
public int cooldownTicks = 0;
|
||||
public int chargeDurationTicks = 0;
|
||||
public int chargeIntervalTicks = 0;
|
||||
public bool startWithMaxHitPoints = true;
|
||||
public bool hitPointsRestoreInstantlyAfterCharge = true;
|
||||
public int rechargeHitPointsIntervalTicks = 60;
|
||||
public bool activated = false;
|
||||
public int activeDuration = 0;
|
||||
public SoundDef activeSound;
|
||||
public bool alwaysShowHitpointsGizmo = false;
|
||||
public float minAlpha = 0f;
|
||||
public float idlePulseSpeed = 0.02f;
|
||||
public float minIdleAlpha = 0.05f;
|
||||
public int disarmedByEmpForTicks = 0;
|
||||
|
||||
public CompProperties_ApparelInterceptor()
|
||||
{
|
||||
compClass = typeof(CompApparelInterceptor);
|
||||
}
|
||||
}
|
||||
|
||||
[StaticConstructorOnStartup]
|
||||
public class CompApparelInterceptor : ThingComp
|
||||
{
|
||||
// 状态变量
|
||||
private int lastInterceptTicks = -999999;
|
||||
private int startedChargingTick = -1;
|
||||
private bool shutDown;
|
||||
private StunHandler stunner;
|
||||
private Sustainer sustainer;
|
||||
public int currentHitPoints = -1;
|
||||
private int ticksToReset;
|
||||
private int activatedTick = -999999;
|
||||
|
||||
// 视觉效果变量
|
||||
private float lastInterceptAngle;
|
||||
private bool drawInterceptCone;
|
||||
|
||||
// 静态资源
|
||||
private static readonly Material ForceFieldMat = MaterialPool.MatFrom("Other/ForceField", ShaderDatabase.MoteGlow);
|
||||
private static readonly Material ForceFieldConeMat = MaterialPool.MatFrom("Other/ForceFieldCone", ShaderDatabase.MoteGlow);
|
||||
private static readonly MaterialPropertyBlock MatPropertyBlock = new MaterialPropertyBlock();
|
||||
private static readonly Color InactiveColor = new Color(0.2f, 0.2f, 0.2f);
|
||||
|
||||
// 属性
|
||||
public CompProperties_ApparelInterceptor Props => (CompProperties_ApparelInterceptor)props;
|
||||
private Pawn PawnOwner => (parent as Apparel)?.Wearer;
|
||||
|
||||
public bool Active
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PawnOwner == null || !PawnOwner.Spawned) return false;
|
||||
if (OnCooldown || Charging || stunner.Stunned || shutDown || currentHitPoints <= 0) return false;
|
||||
if (Props.activated && Find.TickManager.TicksGame > activatedTick + Props.activeDuration) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected bool ShouldDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PawnOwner == null || !PawnOwner.Spawned || PawnOwner.Dead || PawnOwner.Downed || !Active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (PawnOwner.Drafted || PawnOwner.InAggroMentalState || (PawnOwner.Faction != null && PawnOwner.Faction.HostileTo(Faction.OfPlayer) && !PawnOwner.IsPrisoner))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (Find.Selector.IsSelected(PawnOwner))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnCooldown => ticksToReset > 0;
|
||||
public bool Charging => startedChargingTick >= 0 && Find.TickManager.TicksGame < startedChargingTick + Props.startupDelay;
|
||||
public int CooldownTicksLeft => ticksToReset;
|
||||
public int ChargingTicksLeft => (startedChargingTick < 0) ? 0 : Mathf.Max(startedChargingTick + Props.startupDelay - Find.TickManager.TicksGame, 0);
|
||||
public int HitPointsMax => Props.hitPoints;
|
||||
protected virtual int HitPointsPerInterval => 1;
|
||||
|
||||
public override void PostPostMake()
|
||||
{
|
||||
base.PostPostMake();
|
||||
stunner = new StunHandler(parent);
|
||||
if (Props.startupDelay > 0)
|
||||
{
|
||||
startedChargingTick = Find.TickManager.TicksGame;
|
||||
currentHitPoints = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentHitPoints = HitPointsMax;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref lastInterceptTicks, "lastInterceptTicks", -999999);
|
||||
Scribe_Values.Look(ref shutDown, "shutDown", defaultValue: false);
|
||||
Scribe_Values.Look(ref startedChargingTick, "startedChargingTick", -1);
|
||||
Scribe_Values.Look(ref currentHitPoints, "currentHitPoints", -1);
|
||||
Scribe_Values.Look(ref ticksToReset, "ticksToReset", 0);
|
||||
Scribe_Values.Look(ref activatedTick, "activatedTick", -999999);
|
||||
Scribe_Deep.Look(ref stunner, "stunner", parent);
|
||||
|
||||
if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
||||
{
|
||||
if (stunner == null) stunner = new StunHandler(parent);
|
||||
if (currentHitPoints == -1) currentHitPoints = HitPointsMax;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryIntercept(Projectile projectile, Vector3 lastExactPos, Vector3 newExactPos)
|
||||
{
|
||||
if (PawnOwner == null || !PawnOwner.Spawned || !Active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GenGeo.IntersectLineCircleOutline(PawnOwner.Position.ToVector2(), Props.radius, lastExactPos.ToVector2(), newExactPos.ToVector2()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!InterceptsProjectile(Props, projectile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isHostile = (projectile.Launcher != null && projectile.Launcher.HostileTo(PawnOwner)) || (projectile.Launcher == null && Props.interceptNonHostileProjectiles);
|
||||
if (!isHostile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Interception Success ---
|
||||
lastInterceptAngle = projectile.ExactPosition.AngleToFlat(PawnOwner.TrueCenter());
|
||||
lastInterceptTicks = Find.TickManager.TicksGame;
|
||||
drawInterceptCone = true;
|
||||
if (Props.soundInterceptEffecter != null) Props.soundInterceptEffecter.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup();
|
||||
|
||||
if (projectile.DamageDef == DamageDefOf.EMP && !Props.isImmuneToEMP)
|
||||
{
|
||||
BreakShieldEmp(new DamageInfo(projectile.DamageDef, projectile.DamageAmount, instigator: projectile.Launcher));
|
||||
}
|
||||
else if (HitPointsMax > 0)
|
||||
{
|
||||
currentHitPoints -= projectile.DamageAmount;
|
||||
if (currentHitPoints <= 0)
|
||||
{
|
||||
BreakShieldHitpoints(new DamageInfo(projectile.DamageDef, projectile.DamageAmount, instigator: projectile.Launcher));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
if (PawnOwner == null || !PawnOwner.Spawned) return;
|
||||
|
||||
stunner.StunHandlerTick();
|
||||
|
||||
if (OnCooldown)
|
||||
{
|
||||
ticksToReset--;
|
||||
if (ticksToReset <= 0) Reset();
|
||||
}
|
||||
else if (Charging)
|
||||
{
|
||||
// Charging logic handled by property
|
||||
}
|
||||
else if (currentHitPoints < HitPointsMax && parent.IsHashIntervalTick(Props.rechargeHitPointsIntervalTicks))
|
||||
{
|
||||
currentHitPoints = Mathf.Clamp(currentHitPoints + HitPointsPerInterval, 0, HitPointsMax);
|
||||
}
|
||||
|
||||
if (Props.activeSound != null)
|
||||
{
|
||||
if (Active && (sustainer == null || sustainer.Ended)) sustainer = Props.activeSound.TrySpawnSustainer(SoundInfo.InMap(parent));
|
||||
sustainer?.Maintain();
|
||||
if (!Active && sustainer != null && !sustainer.Ended) sustainer.End();
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (PawnOwner.Spawned) Props.reactivateEffect?.Spawn(PawnOwner.Position, PawnOwner.Map).Cleanup();
|
||||
currentHitPoints = HitPointsMax;
|
||||
ticksToReset = 0;
|
||||
}
|
||||
|
||||
private void BreakShieldHitpoints(DamageInfo dinfo)
|
||||
{
|
||||
if (PawnOwner.Spawned)
|
||||
{
|
||||
if (Props.soundBreakEffecter != null) Props.soundBreakEffecter.SpawnAttached(PawnOwner, PawnOwner.MapHeld, Props.radius).Cleanup();
|
||||
}
|
||||
currentHitPoints = 0;
|
||||
ticksToReset = Props.rechargeDelay;
|
||||
}
|
||||
|
||||
private void BreakShieldEmp(DamageInfo dinfo)
|
||||
{
|
||||
BreakShieldHitpoints(dinfo);
|
||||
if (Props.disarmedByEmpForTicks > 0) stunner.Notify_DamageApplied(new DamageInfo(DamageDefOf.EMP, (float)Props.disarmedByEmpForTicks / 30f));
|
||||
}
|
||||
|
||||
public static bool InterceptsProjectile(CompProperties_ApparelInterceptor props, Projectile projectile)
|
||||
{
|
||||
if (projectile.def.projectile.flyOverhead) return props.interceptAirProjectiles;
|
||||
return props.interceptGroundProjectiles;
|
||||
}
|
||||
|
||||
// --- DRAWING LOGIC ---
|
||||
public override void CompDrawWornExtras()
|
||||
{
|
||||
base.CompDrawWornExtras();
|
||||
if (PawnOwner == null || !PawnOwner.Spawned || !ShouldDisplay) return;
|
||||
|
||||
Vector3 drawPos = PawnOwner.Drawer.DrawPos;
|
||||
drawPos.y = AltitudeLayer.MoteOverhead.AltitudeFor();
|
||||
|
||||
float alpha = GetCurrentAlpha();
|
||||
if (alpha > 0f)
|
||||
{
|
||||
Color color = Props.color;
|
||||
color.a *= alpha;
|
||||
MatPropertyBlock.SetColor(ShaderPropertyIDs.Color, color);
|
||||
Matrix4x4 matrix = default(Matrix4x4);
|
||||
matrix.SetTRS(drawPos, Quaternion.identity, new Vector3(Props.radius * 2f * 1.1601562f, 1f, Props.radius * 2f * 1.1601562f));
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix, ForceFieldMat, 0, null, 0, MatPropertyBlock);
|
||||
}
|
||||
|
||||
float coneAlpha = GetCurrentConeAlpha_RecentlyIntercepted();
|
||||
if (coneAlpha > 0f)
|
||||
{
|
||||
Color color = Props.color;
|
||||
color.a *= coneAlpha;
|
||||
MatPropertyBlock.SetColor(ShaderPropertyIDs.Color, color);
|
||||
Matrix4x4 matrix = default(Matrix4x4);
|
||||
matrix.SetTRS(drawPos, Quaternion.Euler(0f, lastInterceptAngle - 90f, 0f), new Vector3(Props.radius * 2f, 1f, Props.radius * 2f));
|
||||
Graphics.DrawMesh(MeshPool.plane10, matrix, ForceFieldConeMat, 0, null, 0, MatPropertyBlock);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetCurrentAlpha()
|
||||
{
|
||||
float idleAlpha = Mathf.Lerp(0.3f, 0.6f, (Mathf.Sin((float)Gen.HashCombineInt(parent.thingIDNumber, 35990913) + Time.realtimeSinceStartup * 2f) + 1f) / 2f);
|
||||
float interceptAlpha = Mathf.Clamp01(1f - (float)(Find.TickManager.TicksGame - lastInterceptTicks) / 40f);
|
||||
return Mathf.Max(idleAlpha, interceptAlpha);
|
||||
}
|
||||
|
||||
private float GetCurrentConeAlpha_RecentlyIntercepted()
|
||||
{
|
||||
if (!drawInterceptCone) return 0f;
|
||||
return Mathf.Clamp01(1f - (float)(Find.TickManager.TicksGame - lastInterceptTicks) / 40f) * 0.82f;
|
||||
}
|
||||
|
||||
// --- GIZMO ---
|
||||
public override IEnumerable<Gizmo> CompGetWornGizmosExtra()
|
||||
{
|
||||
if (PawnOwner != null && Find.Selector.SingleSelectedThing == PawnOwner)
|
||||
{
|
||||
yield return new Gizmo_EnergyShieldStatus { shield = this };
|
||||
}
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (OnCooldown)
|
||||
{
|
||||
sb.Append("Cooldown: " + CooldownTicksLeft.ToStringTicksToPeriod());
|
||||
}
|
||||
else if (stunner.Stunned)
|
||||
{
|
||||
sb.Append("EMP Shutdown: " + stunner.StunTicksLeft.ToStringTicksToPeriod());
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[StaticConstructorOnStartup]
|
||||
public class Gizmo_EnergyShieldStatus : Gizmo
|
||||
{
|
||||
public CompApparelInterceptor shield;
|
||||
private static readonly Texture2D FullShieldBarTex = SolidColorMaterials.NewSolidColorMaterial(new Color(0.2f, 0.8f, 0.85f), ShaderDatabase.MetaOverlay).mainTexture as Texture2D;
|
||||
private static readonly Texture2D EmptyShieldBarTex = SolidColorMaterials.NewSolidColorMaterial(new Color(0.2f, 0.2f, 0.24f), ShaderDatabase.MetaOverlay).mainTexture as Texture2D;
|
||||
|
||||
public override float GetWidth(float maxWidth) => 140f;
|
||||
|
||||
public override GizmoResult GizmoOnGUI(Vector2 topLeft, float maxWidth, GizmoRenderParms parms)
|
||||
{
|
||||
Rect rect = new Rect(topLeft.x, topLeft.y, GetWidth(maxWidth), 75f);
|
||||
Rect rect2 = rect.ContractedBy(6f);
|
||||
Widgets.DrawWindowBackground(rect);
|
||||
|
||||
Rect labelRect = rect2;
|
||||
labelRect.height = rect.height / 2f;
|
||||
Text.Font = GameFont.Tiny;
|
||||
Widgets.Label(labelRect, shield.parent.LabelCap);
|
||||
|
||||
Rect barRect = rect2;
|
||||
barRect.yMin = rect2.y + rect2.height / 2f;
|
||||
float fillPercent = (float)shield.currentHitPoints / shield.HitPointsMax;
|
||||
Widgets.FillableBar(barRect, fillPercent, FullShieldBarTex, EmptyShieldBarTex, false);
|
||||
|
||||
Text.Font = GameFont.Small;
|
||||
Text.Anchor = TextAnchor.MiddleCenter;
|
||||
|
||||
TaggedString statusText = shield.OnCooldown ? "Broken".Translate() : new TaggedString(shield.currentHitPoints + " / " + shield.HitPointsMax);
|
||||
Widgets.Label(barRect, statusText);
|
||||
|
||||
Text.Anchor = TextAnchor.UpperLeft;
|
||||
|
||||
return new GizmoResult(GizmoState.Clear);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Source/WulaFallenEmpire/ThingComp/CompPsychicScaling.cs
Normal file
31
Source/WulaFallenEmpire/ThingComp/CompPsychicScaling.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 这个组件的XML属性定义。允许在XML中配置加成系数。
|
||||
/// </summary>
|
||||
public class CompProperties_PsychicScaling : CompProperties
|
||||
{
|
||||
// 每点心灵敏感度(超出100%的部分)提供的伤害【增伤】乘数。
|
||||
public float damageMultiplierPerSensitivityPoint = 1f;
|
||||
|
||||
// 每点心灵敏感度(低于100%的部分)提供的伤害【减伤】乘数。
|
||||
// 例如,系数为1时,50%敏感度将造成 1 - (1 - 0.5) * 1 = 0.5倍伤害。
|
||||
public float damageReductionMultiplierPerSensitivityPoint = 1f;
|
||||
|
||||
public CompProperties_PsychicScaling()
|
||||
{
|
||||
compClass = typeof(CompPsychicScaling);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 附加到武器上的实际组件。它本身只是一个标记,真正的逻辑在Harmony Patch中。
|
||||
/// </summary>
|
||||
public class CompPsychicScaling : ThingComp
|
||||
{
|
||||
public CompProperties_PsychicScaling Props => (CompProperties_PsychicScaling)props;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompUseEffect_AddDamageShieldCharges : CompUseEffect
|
||||
{
|
||||
public CompProperties_AddDamageShieldCharges Props => (CompProperties_AddDamageShieldCharges)props;
|
||||
|
||||
public override void DoEffect(Pawn user)
|
||||
{
|
||||
base.DoEffect(user);
|
||||
|
||||
// 获取或添加 Hediff_DamageShield
|
||||
Hediff_DamageShield damageShield = user.health.hediffSet.GetFirstHediff<Hediff_DamageShield>();
|
||||
|
||||
if (damageShield == null)
|
||||
{
|
||||
// 如果没有 Hediff,则添加一个
|
||||
damageShield = (Hediff_DamageShield)HediffMaker.MakeHediff(Props.hediffDef, user);
|
||||
user.health.AddHediff(damageShield);
|
||||
damageShield.ShieldCharges = Props.chargesToAdd; // 设置初始层数
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果已有 Hediff,则增加层数
|
||||
damageShield.ShieldCharges += Props.chargesToAdd;
|
||||
}
|
||||
|
||||
// 确保层数不超过最大值
|
||||
if (damageShield.ShieldCharges > (int)damageShield.def.maxSeverity)
|
||||
{
|
||||
damageShield.ShieldCharges = (int)damageShield.def.maxSeverity;
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
Messages.Message("WULA_MessageGainedDamageShieldCharges".Translate(user.LabelShort, Props.chargesToAdd), user, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
|
||||
// 修正 CanBeUsedBy 方法签名
|
||||
public override AcceptanceReport CanBeUsedBy(Pawn p)
|
||||
{
|
||||
// 确保只能对活着的 Pawn 使用
|
||||
if (p.Dead)
|
||||
{
|
||||
return "WULA_CannotUseOnDeadPawn".Translate();
|
||||
}
|
||||
|
||||
// 检查是否已达到最大层数
|
||||
Hediff_DamageShield damageShield = p.health.hediffSet.GetFirstHediff<Hediff_DamageShield>();
|
||||
if (damageShield != null && damageShield.ShieldCharges >= (int)damageShield.def.maxSeverity)
|
||||
{
|
||||
return "WULA_DamageShieldMaxChargesReached".Translate();
|
||||
}
|
||||
|
||||
return true; // 可以使用
|
||||
}
|
||||
|
||||
// 可以在这里添加 GetDescriptionPart() 来显示描述
|
||||
public override string GetDescriptionPart()
|
||||
{
|
||||
return "WULA_DamageShieldChargesDescription".Translate(Props.chargesToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProperties_AddDamageShieldCharges : CompProperties_UseEffect
|
||||
{
|
||||
public HediffDef hediffDef;
|
||||
public int chargesToAdd;
|
||||
|
||||
public CompProperties_AddDamageShieldCharges()
|
||||
{
|
||||
compClass = typeof(CompUseEffect_AddDamageShieldCharges);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_UseEffect_PassionTrainer : CompProperties_UseEffect
|
||||
{
|
||||
public SkillDef skill;
|
||||
public Passion passionGained = Passion.Major; // 可配置获得的热情等级,默认为大火
|
||||
public IntRange passionsLostRange = new IntRange(1, 1); // 可配置失去热情的技能数量范围
|
||||
|
||||
public CompProperties_UseEffect_PassionTrainer()
|
||||
{
|
||||
compClass = typeof(CompUseEffect_PassionTrainer);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompUseEffect_PassionTrainer : CompUseEffect
|
||||
{
|
||||
public CompProperties_UseEffect_PassionTrainer Props => (CompProperties_UseEffect_PassionTrainer)props;
|
||||
|
||||
public override void DoEffect(Pawn usedBy)
|
||||
{
|
||||
base.DoEffect(usedBy);
|
||||
|
||||
if (usedBy.skills == null || Props.skill == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 为指定技能设置热情
|
||||
SkillRecord targetSkillRecord = usedBy.skills.GetSkill(Props.skill);
|
||||
if (targetSkillRecord != null && !targetSkillRecord.TotallyDisabled)
|
||||
{
|
||||
if (targetSkillRecord.passion != Props.passionGained)
|
||||
{
|
||||
targetSkillRecord.passion = Props.passionGained;
|
||||
Messages.Message("WULA_PassionGained".Translate(usedBy.LabelShort, targetSkillRecord.def.label), usedBy, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 确定要移除的热情数量
|
||||
int numToLose = Props.passionsLostRange.RandomInRange;
|
||||
if (numToLose <= 0)
|
||||
{
|
||||
return; // 如果随机到0或更少,则不移除任何热情
|
||||
}
|
||||
|
||||
// 3. 找到所有其他拥有热情的技能
|
||||
List<SkillRecord> skillsWithPassion = usedBy.skills.skills
|
||||
.Where(s => s.def != Props.skill && s.passion > Passion.None && !s.TotallyDisabled)
|
||||
.ToList();
|
||||
|
||||
skillsWithPassion.Shuffle(); // 打乱列表以实现随机性
|
||||
|
||||
// 4. 移除指定数量技能的热情
|
||||
int passionsRemoved = 0;
|
||||
foreach (var skillToLosePassion in skillsWithPassion)
|
||||
{
|
||||
if (passionsRemoved >= numToLose) break;
|
||||
|
||||
skillToLosePassion.passion = Passion.None;
|
||||
Messages.Message("WULA_PassionLost".Translate(usedBy.LabelShort, skillToLosePassion.def.label), usedBy, MessageTypeDefOf.NegativeEvent);
|
||||
passionsRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
public override AcceptanceReport CanBeUsedBy(Pawn p)
|
||||
{
|
||||
if (p.skills == null)
|
||||
{
|
||||
return "PawnHasNoSkills".Translate(p.LabelShort);
|
||||
}
|
||||
if (Props.skill == null)
|
||||
{
|
||||
return "SkillTrainerHasNoSkill".Translate(parent.LabelShort);
|
||||
}
|
||||
if (p.skills.GetSkill(Props.skill).TotallyDisabled)
|
||||
{
|
||||
return "SkillDisabled".Translate();
|
||||
}
|
||||
return base.CanBeUsedBy(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_UseEffect_WulaSkillTrainer : CompProperties_UseEffect
|
||||
{
|
||||
public SkillDef skill; // 目标技能
|
||||
public float xpGainAmount = 50000f; // 目标技能学习量,默认值与原版一致
|
||||
public float baseLossAmount; // 非目标技能基础减少量
|
||||
public float noPassionLossFactor; // 无火技能减少乘数
|
||||
public float minorPassionLossFactor; // 小火技能减少乘数
|
||||
|
||||
public CompProperties_UseEffect_WulaSkillTrainer()
|
||||
{
|
||||
compClass = typeof(CompUseEffect_WulaSkillTrainer);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompUseEffect_WulaSkillTrainer : CompUseEffect
|
||||
{
|
||||
public CompProperties_UseEffect_WulaSkillTrainer Props => (CompProperties_UseEffect_WulaSkillTrainer)props;
|
||||
|
||||
public override void DoEffect(Pawn usedBy)
|
||||
{
|
||||
base.DoEffect(usedBy);
|
||||
|
||||
if (usedBy.skills == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取目标技能
|
||||
SkillDef targetSkill = Props.skill;
|
||||
|
||||
// 遍历所有技能
|
||||
foreach (SkillRecord skillRecord in usedBy.skills.skills)
|
||||
{
|
||||
if (skillRecord.def == targetSkill)
|
||||
{
|
||||
// 目标技能:增加经验
|
||||
skillRecord.Learn(Props.xpGainAmount, true);
|
||||
Messages.Message("WULA_SkillTrainer_TargetSkillGained".Translate(usedBy.LabelShort, skillRecord.def.label), usedBy, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非目标技能:减少经验
|
||||
float experienceLoss = Props.baseLossAmount;
|
||||
if (skillRecord.passion == Passion.None)
|
||||
{
|
||||
experienceLoss *= Props.noPassionLossFactor;
|
||||
}
|
||||
else if (skillRecord.passion == Passion.Minor)
|
||||
{
|
||||
experienceLoss *= Props.minorPassionLossFactor;
|
||||
}
|
||||
// 大火的技能掉得最少,保持默认值
|
||||
|
||||
skillRecord.Learn(-experienceLoss, true); // 减少经验
|
||||
Messages.Message("WULA_SkillTrainer_SkillLoss".Translate(usedBy.LabelShort, skillRecord.def.label), usedBy, MessageTypeDefOf.NegativeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override AcceptanceReport CanBeUsedBy(Pawn p)
|
||||
{
|
||||
if (p.skills == null)
|
||||
{
|
||||
return "PawnHasNoSkills".Translate(p.LabelShort);
|
||||
}
|
||||
if (Props.skill == null)
|
||||
{
|
||||
return "SkillTrainerHasNoSkill".Translate(parent.LabelShort);
|
||||
}
|
||||
// 检查目标技能是否被禁用
|
||||
if (p.skills.GetSkill(Props.skill).TotallyDisabled)
|
||||
{
|
||||
return "SkillDisabled".Translate();
|
||||
}
|
||||
return base.CanBeUsedBy(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompCustomUniqueWeapon : CompUniqueWeapon
|
||||
{
|
||||
// 使用 'new' 关键字来明确隐藏基类成员,解决 CS0108 警告
|
||||
public new CompProperties_CustomUniqueWeapon Props => (CompProperties_CustomUniqueWeapon)props;
|
||||
|
||||
private List<WeaponTraitDef> customTraits = new List<WeaponTraitDef>();
|
||||
|
||||
// 使用 'new' 关键字隐藏基类属性,解决 CS0506 错误
|
||||
public new List<WeaponTraitDef> TraitsListForReading => customTraits;
|
||||
|
||||
// PostExposeData 是 virtual 的,保留 override
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Collections.Look(ref customTraits, "customTraits", LookMode.Def);
|
||||
if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
||||
{
|
||||
if (customTraits == null) customTraits = new List<WeaponTraitDef>();
|
||||
SetupCustomTraits(fromSave: true);
|
||||
}
|
||||
}
|
||||
|
||||
// PostPostMake 是 virtual 的,保留 override
|
||||
public override void PostPostMake()
|
||||
{
|
||||
InitializeCustomTraits();
|
||||
if (parent.TryGetComp<CompQuality>(out var comp))
|
||||
{
|
||||
comp.SetQuality(QualityUtility.GenerateQuality(QualityGenerator.Super), ArtGenerationContext.Outsider);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeCustomTraits()
|
||||
{
|
||||
if (customTraits == null) customTraits = new List<WeaponTraitDef>();
|
||||
customTraits.Clear();
|
||||
|
||||
if (Props.forcedTraits != null)
|
||||
{
|
||||
foreach (var traitToForce in Props.forcedTraits)
|
||||
{
|
||||
if (customTraits.All(t => !t.Overlaps(traitToForce)))
|
||||
{
|
||||
customTraits.Add(traitToForce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IntRange traitRange = Props.numTraitsRange ?? new IntRange(1, 3);
|
||||
int totalTraitsTarget = Mathf.Max(customTraits.Count, traitRange.RandomInRange);
|
||||
int missingTraits = totalTraitsTarget - customTraits.Count;
|
||||
|
||||
if (missingTraits > 0)
|
||||
{
|
||||
// CanAddTrait 现在是我们自己的 'new' 方法
|
||||
IEnumerable<WeaponTraitDef> possibleTraits = DefDatabase<WeaponTraitDef>.AllDefs.Where(CanAddTrait);
|
||||
for (int i = 0; i < missingTraits; i++)
|
||||
{
|
||||
if (!possibleTraits.Any()) break;
|
||||
|
||||
var chosenTrait = possibleTraits.RandomElementByWeight(t => t.commonality);
|
||||
customTraits.Add(chosenTrait);
|
||||
|
||||
possibleTraits = possibleTraits.Where(t => t != chosenTrait && !t.Overlaps(chosenTrait));
|
||||
}
|
||||
}
|
||||
|
||||
SetupCustomTraits(fromSave: false);
|
||||
}
|
||||
|
||||
private void SetupCustomTraits(bool fromSave)
|
||||
{
|
||||
foreach (WeaponTraitDef trait in customTraits)
|
||||
{
|
||||
if (trait.abilityProps != null && parent.GetComp<CompEquippableAbilityReloadable>() is CompEquippableAbilityReloadable comp)
|
||||
{
|
||||
comp.props = trait.abilityProps;
|
||||
if (!fromSave)
|
||||
{
|
||||
comp.Notify_PropsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 'new' 关键字隐藏基类方法,解决 CS0506 错误
|
||||
public new bool CanAddTrait(WeaponTraitDef trait)
|
||||
{
|
||||
if (customTraits.Any(t => t == trait || t.Overlaps(t)))
|
||||
return false;
|
||||
|
||||
if (Props.weaponCategories != null && Props.weaponCategories.Any() && !Props.weaponCategories.Contains(trait.weaponCategory))
|
||||
return false;
|
||||
|
||||
if (customTraits.Count == 0 && !trait.canGenerateAlone)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- 下面的方法都是 virtual 的,保留 override ---
|
||||
|
||||
public override string TransformLabel(string label) => label;
|
||||
public override Color? ForceColor() => null;
|
||||
|
||||
public override float GetStatOffset(StatDef stat) => customTraits.Sum(t => t.statOffsets.GetStatOffsetFromList(stat));
|
||||
public override float GetStatFactor(StatDef stat) => customTraits.Aggregate(1f, (current, t) => current * t.statFactors.GetStatFactorFromList(stat));
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
if (customTraits.NullOrEmpty()) return null;
|
||||
return "WeaponTraits".Translate() + ": " + customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst();
|
||||
}
|
||||
|
||||
public override string CompTipStringExtra()
|
||||
{
|
||||
if (customTraits.NullOrEmpty()) return base.CompTipStringExtra();
|
||||
return "WeaponTraits".Translate() + ": " + customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst();
|
||||
}
|
||||
|
||||
public override IEnumerable<StatDrawEntry> SpecialDisplayStats()
|
||||
{
|
||||
if (customTraits.NullOrEmpty()) yield break;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("Stat_ThingUniqueWeaponTrait_Desc".Translate());
|
||||
builder.AppendLine();
|
||||
|
||||
for (int i = 0; i < customTraits.Count; i++)
|
||||
{
|
||||
WeaponTraitDef trait = customTraits[i];
|
||||
builder.AppendLine(trait.LabelCap.Colorize(ColorLibrary.Yellow));
|
||||
builder.AppendLine(trait.description);
|
||||
if (i < customTraits.Count - 1) builder.AppendLine();
|
||||
}
|
||||
|
||||
yield return new StatDrawEntry(
|
||||
parent.def.IsMeleeWeapon ? StatCategoryDefOf.Weapon_Melee : StatCategoryDefOf.Weapon_Ranged,
|
||||
"Stat_ThingUniqueWeaponTrait_Label".Translate(),
|
||||
customTraits.Select(t => t.label).ToCommaList().CapitalizeFirst(),
|
||||
builder.ToString(),
|
||||
1104);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_CustomUniqueWeapon : CompProperties_UniqueWeapon
|
||||
{
|
||||
// A list of traits that will always be added to the weapon.
|
||||
public List<WeaponTraitDef> forcedTraits;
|
||||
|
||||
// The range of traits to randomly add. If not defined in XML, a default of 1-3 will be used.
|
||||
public IntRange? numTraitsRange;
|
||||
|
||||
public CompProperties_CustomUniqueWeapon()
|
||||
{
|
||||
// Point to the implementation of our custom logic.
|
||||
this.compClass = typeof(CompCustomUniqueWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom CompProperties for our ritual spot. It no longer needs a tag.
|
||||
/// </summary>
|
||||
public class CompProperties_WulaRitualSpot : CompProperties
|
||||
{
|
||||
public CompProperties_WulaRitualSpot()
|
||||
{
|
||||
this.compClass = typeof(CompWulaRitualSpot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The core component for the custom ritual spot. Generates its own gizmos
|
||||
/// by specifically looking for Defs that inherit from our custom PsychicRitualDef_Wula base class.
|
||||
/// </summary>
|
||||
public class CompWulaRitualSpot : ThingComp
|
||||
{
|
||||
public CompProperties_WulaRitualSpot Props => (CompProperties_WulaRitualSpot)this.props;
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
foreach (Gizmo gizmo in base.CompGetGizmosExtra())
|
||||
{
|
||||
yield return gizmo;
|
||||
}
|
||||
|
||||
// Find all rituals that are of our custom base class type.
|
||||
foreach (PsychicRitualDef_Wula ritualDef in DefDatabase<PsychicRitualDef_Wula>.AllDefs)
|
||||
{
|
||||
if (ritualDef.Visible)
|
||||
{
|
||||
Command_Action command_Action = new Command_Action();
|
||||
command_Action.defaultLabel = ritualDef.LabelCap.Resolve();
|
||||
command_Action.defaultDesc = ritualDef.description;
|
||||
command_Action.icon = ritualDef.uiIcon;
|
||||
command_Action.action = delegate
|
||||
{
|
||||
// Mimic vanilla initialization
|
||||
TargetInfo target = new TargetInfo(this.parent);
|
||||
PsychicRitualRoleAssignments assignments = ritualDef.BuildRoleAssignments(target);
|
||||
PsychicRitualCandidatePool candidatePool = ritualDef.FindCandidatePool();
|
||||
ritualDef.InitializeCast(this.parent.Map);
|
||||
Find.WindowStack.Add(new Dialog_BeginPsychicRitual(ritualDef, candidatePool, assignments, this.parent.Map));
|
||||
};
|
||||
|
||||
// Corrected check for cooldown and other requirements
|
||||
AcceptanceReport acceptanceReport = Find.PsychicRitualManager.CanInvoke(ritualDef, this.parent.Map);
|
||||
if (!acceptanceReport.Accepted)
|
||||
{
|
||||
command_Action.Disable(acceptanceReport.Reason.CapitalizeFirst());
|
||||
}
|
||||
|
||||
yield return command_Action;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PsychicRitualDef_AddHediff : PsychicRitualDef_Wula
|
||||
{
|
||||
public HediffDef hediff;
|
||||
|
||||
public override List<PsychicRitualToil> CreateToils(PsychicRitual psychicRitual, PsychicRitualGraph parent)
|
||||
{
|
||||
List<PsychicRitualToil> list = base.CreateToils(psychicRitual, parent);
|
||||
list.Add(new PsychicRitualToil_AddHediff(TargetRole, hediff));
|
||||
list.Add(new PsychicRitualToil_TargetCleanup(InvokerRole, TargetRole));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using RimWorld;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// This class serves as a custom base for all Wula rituals.
|
||||
/// It inherits from PsychicRitualDef_InvocationCircle to retain all vanilla functionality,
|
||||
/// but provides a unique type that our custom CompWulaRitualSpot can specifically look for,
|
||||
/// ensuring these rituals only appear on our custom ritual spot.
|
||||
/// </summary>
|
||||
public class PsychicRitualDef_Wula : PsychicRitualDef_WulaBase
|
||||
{
|
||||
// This class can be expanded with Wula-specific ritual properties if needed in the future.
|
||||
// For now, its existence is enough to separate our rituals from the vanilla ones.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
using Verse.AI;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PsychicRitualDef_WulaBase : PsychicRitualDef
|
||||
{
|
||||
public enum InvalidTargetReasonEnum
|
||||
{
|
||||
None,
|
||||
AreaNotClear
|
||||
}
|
||||
|
||||
private class RitualQualityOffsetCount
|
||||
{
|
||||
public float offset;
|
||||
public int count;
|
||||
|
||||
public RitualQualityOffsetCount(int count, float offset)
|
||||
{
|
||||
this.count = count;
|
||||
this.offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
public FloatRange hoursUntilHoraxEffect;
|
||||
public FloatRange hoursUntilOutcome;
|
||||
public float invocationCircleRadius = 3.9f;
|
||||
[MustTranslate]
|
||||
public string outcomeDescription;
|
||||
public float psychicSensitivityPowerFactor = 0.25f;
|
||||
protected PsychicRitualRoleDef invokerRole;
|
||||
protected PsychicRitualRoleDef chanterRole;
|
||||
protected PsychicRitualRoleDef targetRole;
|
||||
protected PsychicRitualRoleDef defenderRole;
|
||||
protected IngredientCount requiredOffering;
|
||||
protected string timeAndOfferingLabelCached;
|
||||
|
||||
public static readonly SimpleCurve PsychicSensitivityToPowerFactor = new SimpleCurve
|
||||
{
|
||||
new CurvePoint(0f, 0f),
|
||||
new CurvePoint(1f, 0.5f),
|
||||
new CurvePoint(2f, 0.9f),
|
||||
new CurvePoint(3f, 1f)
|
||||
};
|
||||
|
||||
protected const int DurationTicksWaitPostEffect = 120;
|
||||
private static Dictionary<PsychicRitualRoleDef, List<IntVec3>> tmpParticipants = new Dictionary<PsychicRitualRoleDef, List<IntVec3>>(8);
|
||||
private List<Pawn> tmpGatheringPawns = new List<Pawn>(8);
|
||||
|
||||
public virtual PsychicRitualRoleDef InvokerRole => invokerRole;
|
||||
public virtual PsychicRitualRoleDef ChanterRole => chanterRole;
|
||||
public virtual PsychicRitualRoleDef TargetRole => targetRole;
|
||||
public virtual PsychicRitualRoleDef DefenderRole => defenderRole;
|
||||
public virtual IngredientCount RequiredOffering => requiredOffering;
|
||||
public TaggedString CooldownLabel => "PsychicRitualCooldownLabel".Translate() + ": " + (cooldownHours * 2500).ToStringTicksToPeriod();
|
||||
|
||||
public override List<PsychicRitualRoleDef> Roles
|
||||
{
|
||||
get
|
||||
{
|
||||
List<PsychicRitualRoleDef> roles = base.Roles;
|
||||
if (InvokerRole != null) roles.Add(InvokerRole);
|
||||
if (TargetRole != null) roles.Add(TargetRole);
|
||||
if (ChanterRole != null) roles.Add(ChanterRole);
|
||||
if (DefenderRole != null) roles.Add(DefenderRole);
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ResolveReferences()
|
||||
{
|
||||
base.ResolveReferences();
|
||||
requiredOffering?.ResolveReferences();
|
||||
invokerRole = invokerRole ?? PsychicRitualRoleDefOf.Invoker;
|
||||
chanterRole = chanterRole ?? PsychicRitualRoleDefOf.Chanter;
|
||||
}
|
||||
|
||||
public override List<PsychicRitualToil> CreateToils(PsychicRitual psychicRitual, PsychicRitualGraph parent)
|
||||
{
|
||||
float randomInRange = hoursUntilOutcome.RandomInRange;
|
||||
IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> readOnlyDictionary = GenerateRolePositions(psychicRitual.assignments);
|
||||
return new List<PsychicRitualToil>
|
||||
{
|
||||
new PsychicRitualToil_GatherForInvocation_Wula(psychicRitual, this, readOnlyDictionary),
|
||||
new PsychicRitualToil_InvokeHorax(InvokerRole, readOnlyDictionary.TryGetValue(InvokerRole), TargetRole, readOnlyDictionary.TryGetValue(TargetRole), ChanterRole, readOnlyDictionary.TryGetValue(ChanterRole), DefenderRole, readOnlyDictionary.TryGetValue(DefenderRole), RequiredOffering)
|
||||
{
|
||||
hoursUntilHoraxEffect = hoursUntilHoraxEffect.RandomInRange,
|
||||
hoursUntilOutcome = randomInRange
|
||||
},
|
||||
new PsychicRitualToil_Wait(120)
|
||||
};
|
||||
}
|
||||
|
||||
public override bool IsValidTarget(TargetInfo target, out AnyEnum reason)
|
||||
{
|
||||
foreach (IntVec3 item in GenRadial.RadialCellsAround(target.Cell, invocationCircleRadius, useCenter: true))
|
||||
{
|
||||
if (!item.Standable(target.Map))
|
||||
{
|
||||
reason = AnyEnum.FromEnum(InvalidTargetReasonEnum.AreaNotClear);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
reason = AnyEnum.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override TaggedString InvalidTargetReason(AnyEnum reason)
|
||||
{
|
||||
InvalidTargetReasonEnum? invalidTargetReasonEnum = reason.As<InvalidTargetReasonEnum>();
|
||||
if (invalidTargetReasonEnum.HasValue)
|
||||
{
|
||||
InvalidTargetReasonEnum valueOrDefault = invalidTargetReasonEnum.GetValueOrDefault();
|
||||
return valueOrDefault switch
|
||||
{
|
||||
InvalidTargetReasonEnum.None => TaggedString.Empty,
|
||||
InvalidTargetReasonEnum.AreaNotClear => "PsychicRitualDef_InvocationCircle_AreaMustBeClear".Translate(),
|
||||
_ => throw new System.InvalidOperationException($"Unknown reason {valueOrDefault}"),
|
||||
};
|
||||
}
|
||||
return base.InvalidTargetReason(reason);
|
||||
}
|
||||
|
||||
public override TaggedString OutcomeDescription(FloatRange qualityRange, string qualityNumber, PsychicRitualRoleAssignments assignments)
|
||||
{
|
||||
return outcomeDescription.Formatted();
|
||||
}
|
||||
|
||||
public override IEnumerable<TaggedString> OutcomeWarnings(PsychicRitualRoleAssignments assignments)
|
||||
{
|
||||
foreach (Pawn item in assignments.AssignedPawns(TargetRole))
|
||||
{
|
||||
if (item.HomeFaction != null && item.HomeFaction != Faction.OfPlayer && item.HomeFaction.def.humanlikeFaction && !item.HomeFaction.def.PermanentlyHostileTo(FactionDefOf.PlayerColony) && !item.HomeFaction.temporary && !item.HomeFaction.Hidden)
|
||||
{
|
||||
yield return "PsychicRitualFactionWarning".Translate(item.Named("PAWN"), item.HomeFaction.Named("FACTION")).Colorize(ColoredText.WarningColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override TaggedString TimeAndOfferingLabel()
|
||||
{
|
||||
if (timeAndOfferingLabelCached != null)
|
||||
{
|
||||
return timeAndOfferingLabelCached;
|
||||
}
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine(DurationLabel());
|
||||
stringBuilder.Append(CooldownLabel);
|
||||
if (!OfferingLabel().NullOrEmpty())
|
||||
{
|
||||
stringBuilder.AppendLine();
|
||||
stringBuilder.Append(OfferingLabel());
|
||||
}
|
||||
timeAndOfferingLabelCached = stringBuilder.ToString();
|
||||
return timeAndOfferingLabelCached;
|
||||
}
|
||||
|
||||
private TaggedString OfferingLabel()
|
||||
{
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
if (RequiredOffering != null)
|
||||
{
|
||||
stringBuilder.Append("PsychicRitualRequiredOffering".Translate().CapitalizeFirst());
|
||||
stringBuilder.Append(": ");
|
||||
stringBuilder.Append(RequiredOffering.SummaryFilterFirst);
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public TaggedString DurationLabel()
|
||||
{
|
||||
string value = ((int)(hoursUntilOutcome.Average * 2500f)).ToStringTicksToPeriod();
|
||||
TaggedString taggedString = ((hoursUntilOutcome.min != hoursUntilOutcome.max) ? "ExpectedLordJobDuration".Translate().CapitalizeFirst() : "PsychicRitualExpectedDurationLabel".Translate().CapitalizeFirst());
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
stringBuilder.Append(taggedString);
|
||||
stringBuilder.Append(": ");
|
||||
stringBuilder.Append(value);
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> GenerateRolePositions(PsychicRitualRoleAssignments assignments)
|
||||
{
|
||||
tmpParticipants.ClearAndPoolValueLists();
|
||||
foreach (PsychicRitualRoleDef role in Roles)
|
||||
{
|
||||
tmpParticipants[role] = SimplePool<List<IntVec3>>.Get();
|
||||
}
|
||||
int num = assignments.RoleAssignedCount(ChanterRole) + assignments.RoleAssignedCount(InvokerRole);
|
||||
int num2 = 0;
|
||||
foreach (Pawn item in assignments.AssignedPawns(InvokerRole))
|
||||
{
|
||||
_ = item;
|
||||
int num3 = 0;
|
||||
IntVec3 cell;
|
||||
do
|
||||
{
|
||||
cell = assignments.Target.Cell;
|
||||
cell += IntVec3.FromPolar(360f * (float)num2++ / (float)num, invocationCircleRadius);
|
||||
}
|
||||
while (!cell.Walkable(assignments.Target.Map) && num3++ <= 10);
|
||||
if (num3 >= 10)
|
||||
{
|
||||
cell = assignments.Target.Cell;
|
||||
}
|
||||
tmpParticipants[InvokerRole].Add(cell);
|
||||
}
|
||||
foreach (Pawn item2 in assignments.AssignedPawns(ChanterRole))
|
||||
{
|
||||
_ = item2;
|
||||
IntVec3 cell2 = assignments.Target.Cell;
|
||||
cell2 += IntVec3.FromPolar(360f * (float)num2++ / (float)num, invocationCircleRadius);
|
||||
tmpParticipants[ChanterRole].Add(cell2);
|
||||
}
|
||||
foreach (Pawn item3 in assignments.AssignedPawns(TargetRole))
|
||||
{
|
||||
_ = item3;
|
||||
tmpParticipants[TargetRole].Add(assignments.Target.Cell);
|
||||
}
|
||||
if (DefenderRole != null)
|
||||
{
|
||||
num2 = 0;
|
||||
int num4 = assignments.RoleAssignedCount(DefenderRole);
|
||||
bool playerRitual = assignments.AllAssignedPawns.Any((Pawn x) => x.Faction == Faction.OfPlayer);
|
||||
foreach (Pawn item4 in assignments.AssignedPawns(DefenderRole))
|
||||
{
|
||||
_ = item4;
|
||||
IntVec3 cell3 = assignments.Target.Cell;
|
||||
cell3 += IntVec3.FromPolar(360f * (float)num2++ / (float)num4, invocationCircleRadius + 5f);
|
||||
cell3 = GetBestStandableRolePosition(playerRitual, cell3, assignments.Target.Cell, assignments.Target.Map);
|
||||
tmpParticipants[DefenderRole].Add(cell3);
|
||||
}
|
||||
}
|
||||
return tmpParticipants;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> BlockingIssues(PsychicRitualRoleAssignments assignments, Map map)
|
||||
{
|
||||
using (new ProfilerBlock("PsychicRitualDef.BlockingIssues"))
|
||||
{
|
||||
tmpGatheringPawns.Clear();
|
||||
foreach (var (psychicRitualRoleDef2, collection) in assignments.RoleAssignments)
|
||||
{
|
||||
if (psychicRitualRoleDef2.CanHandleOfferings)
|
||||
{
|
||||
tmpGatheringPawns.AddRange(collection);
|
||||
}
|
||||
}
|
||||
tmpGatheringPawns.RemoveAll(map, (Map _map, Pawn _pawn) => _pawn.MapHeld != _map);
|
||||
if (TargetRole != null && InvokerRole != null)
|
||||
{
|
||||
Pawn pawn = assignments.FirstAssignedPawn(TargetRole);
|
||||
if (pawn != null)
|
||||
{
|
||||
Pawn pawn2 = assignments.FirstAssignedPawn(InvokerRole);
|
||||
if (pawn2 != null && pawn.IsPrisoner && !map.reachability.CanReach(assignments.Target.Cell, pawn.PositionHeld, PathEndMode.Touch, TraverseParms.For(pawn2)))
|
||||
{
|
||||
yield return "PsychicRitualTargetUnreachableByInvoker".Translate(pawn.Named("TARGET"), pawn2.Named("INVOKER"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (RequiredOffering != null && !PsychicRitualDef.OfferingReachable(map, tmpGatheringPawns, RequiredOffering, out var reachableCount))
|
||||
{
|
||||
yield return "PsychicRitualOfferingsInsufficient".Translate(RequiredOffering.SummaryFilterFirst, reachableCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void CalculateMaxPower(PsychicRitualRoleAssignments assignments, List<QualityFactor> powerFactorsOut, out float power)
|
||||
{
|
||||
power = 0f;
|
||||
foreach (Pawn item in assignments.AssignedPawns(InvokerRole))
|
||||
{
|
||||
float statValue = item.GetStatValue(StatDefOf.PsychicSensitivity);
|
||||
float num = PsychicSensitivityToPowerFactor.Evaluate(statValue);
|
||||
num *= psychicSensitivityPowerFactor;
|
||||
powerFactorsOut?.Add(new QualityFactor
|
||||
{
|
||||
label = "PsychicRitualDef_InvocationCircle_QualityFactor_PsychicSensitivity".Translate(item.Named("PAWN")),
|
||||
positive = (statValue >= 1f),
|
||||
count = statValue.ToStringPercent(),
|
||||
quality = num,
|
||||
toolTip = "PsychicRitualDef_InvocationCircle_QualityFactor_PsychicSensitivity_Tooltip".Translate(item.Named("PAWN"))
|
||||
});
|
||||
power += num;
|
||||
}
|
||||
base.CalculateMaxPower(assignments, powerFactorsOut, out var power2);
|
||||
power += power2;
|
||||
if (assignments.Target.Thing is Building building)
|
||||
{
|
||||
CalculateFacilityQualityOffset(powerFactorsOut, ref power, building);
|
||||
}
|
||||
power = Mathf.Clamp01(power);
|
||||
}
|
||||
|
||||
private static void CalculateFacilityQualityOffset(List<QualityFactor> powerFactorsOut, ref float power, Building building)
|
||||
{
|
||||
Dictionary<ThingDef, RitualQualityOffsetCount> dictionary = new Dictionary<ThingDef, RitualQualityOffsetCount>();
|
||||
List<Thing> linkedFacilitiesListForReading = building.GetComp<CompAffectedByFacilities>().LinkedFacilitiesListForReading;
|
||||
for (int i = 0; i < linkedFacilitiesListForReading.Count; i++)
|
||||
{
|
||||
Thing thing = linkedFacilitiesListForReading[i];
|
||||
CompFacility compFacility = thing.TryGetComp<CompFacility>();
|
||||
if (compFacility?.StatOffsets == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < compFacility.StatOffsets.Count; j++)
|
||||
{
|
||||
StatModifier statModifier = compFacility.StatOffsets[j];
|
||||
if (statModifier.stat == StatDefOf.PsychicRitualQuality)
|
||||
{
|
||||
if (dictionary.TryGetValue(thing.def, out var value))
|
||||
{
|
||||
value.count++;
|
||||
value.offset += statModifier.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
dictionary.Add(thing.def, new RitualQualityOffsetCount(1, statModifier.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (KeyValuePair<ThingDef, RitualQualityOffsetCount> item in dictionary)
|
||||
{
|
||||
powerFactorsOut?.Add(new QualityFactor
|
||||
{
|
||||
label = Find.ActiveLanguageWorker.Pluralize(item.Key.label).CapitalizeFirst(),
|
||||
positive = true,
|
||||
count = item.Value.count + " / " + item.Key.GetCompProperties<CompProperties_Facility>().maxSimultaneous,
|
||||
quality = item.Value.offset,
|
||||
toolTip = "PsychicRitualDef_InvocationCircle_QualityFactor_Increase_Tooltip".Translate().CapitalizeFirst().EndWithPeriod()
|
||||
});
|
||||
power += item.Value.offset;
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<StatDrawEntry> SpecialDisplayStats(StatRequest req)
|
||||
{
|
||||
foreach (StatDrawEntry item in base.SpecialDisplayStats(req))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
if (requiredOffering != null)
|
||||
{
|
||||
yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_Offering".Translate(), requiredOffering.SummaryFilterFirst, "StatsReport_Offering_Desc".Translate(), 1000);
|
||||
}
|
||||
yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_RitualDuration".Translate(), Mathf.FloorToInt(hoursUntilOutcome.min * 2500f).ToStringTicksToPeriod(), "StatsReport_RitualDuration_Desc".Translate(), 500);
|
||||
yield return new StatDrawEntry(StatCategoryDefOf.PsychicRituals, "StatsReport_RitualCooldown".Translate(), (cooldownHours * 2500).ToStringTicksToPeriod(), "StatsReport_RitualCooldown_Desc".Translate(), 100);
|
||||
}
|
||||
|
||||
public override void CheckPsychicRitualCancelConditions(PsychicRitual psychicRitual)
|
||||
{
|
||||
base.CheckPsychicRitualCancelConditions(psychicRitual);
|
||||
if (!psychicRitual.canceled && invokerRole != null)
|
||||
{
|
||||
Pawn pawn = psychicRitual.assignments.FirstAssignedPawn(InvokerRole);
|
||||
if (pawn != null && pawn.DeadOrDowned)
|
||||
{
|
||||
psychicRitual.CancelPsychicRitual("PsychicRitualDef_InvocationCircle_InvokerLost".Translate(pawn.Named("PAWN")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PsychicRitualToil_AddHediff : PsychicRitualToil
|
||||
{
|
||||
public PsychicRitualRoleDef targetRole;
|
||||
public HediffDef hediff;
|
||||
|
||||
private static List<Pawn> tmpTargetPawns = new List<Pawn>(4);
|
||||
|
||||
public PsychicRitualToil_AddHediff()
|
||||
{
|
||||
}
|
||||
|
||||
public PsychicRitualToil_AddHediff(PsychicRitualRoleDef targetRole, HediffDef hediff)
|
||||
{
|
||||
this.targetRole = targetRole;
|
||||
this.hediff = hediff;
|
||||
}
|
||||
|
||||
public override void Start(PsychicRitual psychicRitual, PsychicRitualGraph graph)
|
||||
{
|
||||
tmpTargetPawns.Clear();
|
||||
tmpTargetPawns.AddRange(psychicRitual.assignments.AssignedPawns(targetRole));
|
||||
foreach (Pawn tmpTargetPawn in tmpTargetPawns)
|
||||
{
|
||||
ApplyOutcome(psychicRitual, tmpTargetPawn);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyOutcome(PsychicRitual psychicRitual, Pawn pawn)
|
||||
{
|
||||
if (hediff != null)
|
||||
{
|
||||
pawn.health.AddHediff(hediff);
|
||||
}
|
||||
|
||||
if (PawnUtility.ShouldSendNotificationAbout(pawn))
|
||||
{
|
||||
Find.LetterStack.ReceiveLetter("PsychicRitualCompleteLabel".Translate(psychicRitual.def.label), ((PsychicRitualDef_AddHediff)psychicRitual.def).outcomeDescription.Formatted(pawn.Named("PAWN")), LetterDefOf.NeutralEvent, pawn);
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_Defs.Look(ref targetRole, "targetRole");
|
||||
Scribe_Defs.Look(ref hediff, "hediff");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class PsychicRitualToil_GatherForInvocation_Wula : PsychicRitualToil_Multiplex
|
||||
{
|
||||
protected PsychicRitualToil_Goto fallbackToil;
|
||||
protected PsychicRitualGraph invokerToil;
|
||||
protected PsychicRitualToil_Goto invokerFinalToil;
|
||||
private static List<Pawn> blockingPawns = new List<Pawn>(16);
|
||||
|
||||
protected PsychicRitualToil_GatherForInvocation_Wula() { }
|
||||
|
||||
protected PsychicRitualToil_GatherForInvocation_Wula(PsychicRitualDef_WulaBase def, PsychicRitualToil_Goto fallbackToil, PsychicRitualGraph invokerToil)
|
||||
: base(new Dictionary<PsychicRitualRoleDef, PsychicRitualToil> { { def.InvokerRole, invokerToil } }, fallbackToil)
|
||||
{
|
||||
this.fallbackToil = fallbackToil;
|
||||
this.invokerToil = invokerToil;
|
||||
invokerFinalToil = (PsychicRitualToil_Goto)invokerToil.GetToil(invokerToil.ToilCount - 1);
|
||||
}
|
||||
|
||||
public PsychicRitualToil_GatherForInvocation_Wula(PsychicRitual psychicRitual, PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
|
||||
: this(def, FallbackToil(psychicRitual, def, rolePositions), InvokerToil(def, rolePositions))
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_References.Look(ref fallbackToil, "fallbackToil");
|
||||
Scribe_References.Look(ref invokerToil, "invokerToil");
|
||||
Scribe_References.Look(ref invokerFinalToil, "invokerFinalToil");
|
||||
}
|
||||
|
||||
public override string GetReport(PsychicRitual psychicRitual, PsychicRitualGraph parent)
|
||||
{
|
||||
blockingPawns.Clear();
|
||||
blockingPawns.AddRange(fallbackToil.BlockingPawns);
|
||||
if (invokerToil.CurrentToil == invokerFinalToil)
|
||||
{
|
||||
blockingPawns.AddRange(invokerFinalToil.BlockingPawns);
|
||||
}
|
||||
else
|
||||
{
|
||||
blockingPawns.AddRange(invokerFinalToil.ControlledPawns(psychicRitual));
|
||||
}
|
||||
string text = "PsychicRitualToil_GatherForInvocation_Report".Translate();
|
||||
string text2 = blockingPawns.Select((Pawn pawn) => pawn.LabelShortCap).ToCommaList();
|
||||
return text + ": " + text2;
|
||||
}
|
||||
|
||||
public static PsychicRitualToil_Goto FallbackToil(PsychicRitual psychicRitual, PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
|
||||
{
|
||||
return new PsychicRitualToil_Goto(rolePositions.Slice(rolePositions.Keys.Except(def.InvokerRole)));
|
||||
}
|
||||
|
||||
public static PsychicRitualGraph InvokerToil(PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
|
||||
{
|
||||
return new PsychicRitualGraph(InvokerGatherPhaseToils(def, rolePositions))
|
||||
{
|
||||
willAdvancePastLastToil = false
|
||||
};
|
||||
}
|
||||
|
||||
public static IEnumerable<PsychicRitualToil> InvokerGatherPhaseToils(PsychicRitualDef_WulaBase def, IReadOnlyDictionary<PsychicRitualRoleDef, List<IntVec3>> rolePositions)
|
||||
{
|
||||
if (def.RequiredOffering != null)
|
||||
{
|
||||
yield return new PsychicRitualToil_GatherOfferings(def.InvokerRole, def.RequiredOffering);
|
||||
}
|
||||
if (def.TargetRole != null)
|
||||
{
|
||||
yield return new PsychicRitualToil_CarryAndGoto(def.InvokerRole, def.TargetRole, rolePositions);
|
||||
}
|
||||
yield return new PsychicRitualToil_Goto(rolePositions.Slice(def.InvokerRole));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 用于在XML中定义祭品
|
||||
public class OfferingItem
|
||||
{
|
||||
public ThingDef thingDef;
|
||||
public float power;
|
||||
}
|
||||
|
||||
public class QualityThreshold
|
||||
{
|
||||
public float threshold;
|
||||
public QualityCategory quality;
|
||||
}
|
||||
|
||||
public class PsychicRitual_TechOffering : PsychicRitualDef_Wula
|
||||
{
|
||||
// 从XML加载的额外祭品列表
|
||||
public List<OfferingItem> extraOfferings = new List<OfferingItem>();
|
||||
|
||||
// 从XML加载的奖励池
|
||||
public List<ThingDef> rewardWeaponPool = new List<ThingDef>();
|
||||
|
||||
// 从XML加载的品质阈值
|
||||
public List<QualityThreshold> qualityThresholds = new List<QualityThreshold>();
|
||||
|
||||
// 重写计算最大能量的方法
|
||||
public override void CalculateMaxPower(PsychicRitualRoleAssignments assignments, List<QualityFactor> powerFactorsOut, out float power)
|
||||
{
|
||||
// 首先调用基类方法
|
||||
base.CalculateMaxPower(assignments, powerFactorsOut, out power);
|
||||
|
||||
IntVec3 center = assignments.Target.Cell;
|
||||
Map map = assignments.Target.Map;
|
||||
float offeringRadius = 8f;
|
||||
var thingsInRadius = GenRadial.RadialDistinctThingsAround(center, map, offeringRadius, useCenter: true).ToList();
|
||||
|
||||
// 创建一个可变的必需品计数器
|
||||
var requiredCounts = new Dictionary<ThingDef, int>();
|
||||
if (this.requiredOffering != null)
|
||||
{
|
||||
foreach (ThingDef thingDef in this.requiredOffering.filter.AllowedThingDefs)
|
||||
{
|
||||
requiredCounts[thingDef] = (int)this.requiredOffering.GetBaseCount();
|
||||
}
|
||||
}
|
||||
|
||||
float extraPowerFromOfferings = 0f;
|
||||
int offeringItemsCount = 0;
|
||||
|
||||
if (!extraOfferings.NullOrEmpty())
|
||||
{
|
||||
var extraOfferingInfo = extraOfferings.ToDictionary(o => o.thingDef, o => o.power);
|
||||
|
||||
// 遍历仪式范围内的所有物品
|
||||
foreach (Thing thing in thingsInRadius)
|
||||
{
|
||||
// 检查这个物品是否可以作为额外祭品
|
||||
if (extraOfferingInfo.TryGetValue(thing.def, out float powerPerItem))
|
||||
{
|
||||
int countInStack = thing.stackCount;
|
||||
|
||||
// 检查这个物品是否是必需品,并扣除相应数量
|
||||
if (requiredCounts.TryGetValue(thing.def, out int requiredCount) && requiredCount > 0)
|
||||
{
|
||||
int numToFulfillRequirement = System.Math.Min(countInStack, requiredCount);
|
||||
requiredCounts[thing.def] -= numToFulfillRequirement;
|
||||
countInStack -= numToFulfillRequirement;
|
||||
}
|
||||
|
||||
// 任何剩余的物品都算作额外祭品
|
||||
if (countInStack > 0)
|
||||
{
|
||||
extraPowerFromOfferings += powerPerItem * countInStack;
|
||||
offeringItemsCount += countInStack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加UI显示元素
|
||||
powerFactorsOut?.Add(new QualityFactor
|
||||
{
|
||||
label = "WULA_ExtraOfferings".Translate(),
|
||||
positive = offeringItemsCount > 0,
|
||||
quality = extraPowerFromOfferings,
|
||||
toolTip = "WULA_ExtraOfferings_Tooltip".Translate(),
|
||||
count = offeringItemsCount > 0 ? "✓" : "✗" // 使用对勾/叉号来清晰显示状态
|
||||
});
|
||||
}
|
||||
|
||||
power += extraPowerFromOfferings;
|
||||
power = UnityEngine.Mathf.Clamp01(power);
|
||||
}
|
||||
|
||||
// 重写创建仪式步骤的方法
|
||||
public override List<PsychicRitualToil> CreateToils(PsychicRitual psychicRitual, PsychicRitualGraph parent)
|
||||
{
|
||||
// 获取基类的仪式步骤,这其中已经包含了等待 hoursUntilOutcome 的逻辑
|
||||
List<PsychicRitualToil> toils = base.CreateToils(psychicRitual, parent);
|
||||
|
||||
// 在所有基类步骤之后,添加我们自定义的奖励步骤
|
||||
toils.Add(new PsychicRitualToil_TechOfferingOutcome(this));
|
||||
|
||||
return toils;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义的仪式步骤,用于处理奖励
|
||||
public class PsychicRitualToil_TechOfferingOutcome : PsychicRitualToil
|
||||
{
|
||||
private PsychicRitual_TechOffering ritualDef;
|
||||
|
||||
// 需要一个无参构造函数用于序列化
|
||||
public PsychicRitualToil_TechOfferingOutcome() { }
|
||||
|
||||
public PsychicRitualToil_TechOfferingOutcome(PsychicRitual_TechOffering def)
|
||||
{
|
||||
this.ritualDef = def;
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_Defs.Look(ref ritualDef, "ritualDef");
|
||||
}
|
||||
|
||||
public override void Start(PsychicRitual psychicRitual, PsychicRitualGraph graph)
|
||||
{
|
||||
float power = psychicRitual.PowerPercent;
|
||||
|
||||
// 消耗祭品
|
||||
IntVec3 center = psychicRitual.assignments.Target.Cell;
|
||||
Map map = psychicRitual.assignments.Target.Map;
|
||||
float offeringRadius = 8f;
|
||||
|
||||
if (!ritualDef.extraOfferings.NullOrEmpty())
|
||||
{
|
||||
var offeringThings = new Dictionary<ThingDef, float>();
|
||||
foreach(var offering in ritualDef.extraOfferings)
|
||||
{
|
||||
offeringThings[offering.thingDef] = offering.power;
|
||||
}
|
||||
|
||||
foreach (Thing thing in GenRadial.RadialDistinctThingsAround(center, map, offeringRadius, useCenter: true))
|
||||
{
|
||||
if (offeringThings.ContainsKey(thing.def))
|
||||
{
|
||||
thing.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从奖励池中随机选择一个武器
|
||||
if (ritualDef.rewardWeaponPool.NullOrEmpty())
|
||||
{
|
||||
Log.Error($"[WulaFallenEmpire] Reward weapon pool is empty for {ritualDef.defName}");
|
||||
return;
|
||||
}
|
||||
ThingDef weaponDef = ritualDef.rewardWeaponPool.RandomElement();
|
||||
if (weaponDef == null)
|
||||
{
|
||||
Log.Error($"[WulaFallenEmpire] Could not find weapon Def in reward pool for {ritualDef.defName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据能量值决定物品品质
|
||||
QualityCategory quality = QualityCategory.Awful; // 默认最低品质
|
||||
if (!ritualDef.qualityThresholds.NullOrEmpty())
|
||||
{
|
||||
var sortedThresholds = ritualDef.qualityThresholds.OrderByDescending(t => t.threshold).ToList();
|
||||
foreach (var threshold in sortedThresholds)
|
||||
{
|
||||
if (power >= threshold.threshold)
|
||||
{
|
||||
quality = threshold.quality;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (power >= 1.0f) { quality = QualityCategory.Legendary; }
|
||||
else if (power >= 0.8f) { quality = QualityCategory.Masterwork; }
|
||||
else if (power >= 0.5f) { quality = QualityCategory.Excellent; }
|
||||
else if (power >= 0.2f) { quality = QualityCategory.Normal; }
|
||||
else { quality = QualityCategory.Poor; }
|
||||
}
|
||||
|
||||
// 创建物品并设置品质
|
||||
Thing reward = ThingMaker.MakeThing(weaponDef);
|
||||
if (reward.TryGetComp<CompQuality>() is CompQuality compQuality)
|
||||
{
|
||||
compQuality.SetQuality(quality, ArtGenerationContext.Colony);
|
||||
}
|
||||
|
||||
// 在仪式中心点生成奖励物品
|
||||
GenPlace.TryPlaceThing(reward, psychicRitual.assignments.Target.Cell, map, ThingPlaceMode.Near);
|
||||
|
||||
// 发送消息通知玩家
|
||||
Find.LetterStack.ReceiveLetter(
|
||||
"WULA_RitualReward_Label".Translate(),
|
||||
"WULA_RitualReward_Description".Translate(reward.Label, quality.GetLabel()),
|
||||
LetterDefOf.PositiveEvent,
|
||||
new LookTargets(psychicRitual.assignments.Target.Cell, map)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class RitualTagExtension : DefModExtension
|
||||
{
|
||||
public string ritualTag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using System.Reflection; // For InnerThing reflection if needed, but innerContainer is directly accessible
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// ArmedShuttleIncoming now directly implements the logic from PassengerShuttleIncoming
|
||||
// It should inherit from ShuttleIncoming, as PassengerShuttleIncoming does.
|
||||
public class ArmedShuttleIncoming : ShuttleIncoming // Changed from PassengerShuttleIncoming
|
||||
{
|
||||
private static readonly SimpleCurve AngleCurve = new SimpleCurve
|
||||
{
|
||||
new CurvePoint(0f, 30f),
|
||||
new CurvePoint(1f, 0f)
|
||||
};
|
||||
|
||||
// innerContainer is a protected field in Skyfaller, accessible to derived classes like ShuttleIncoming
|
||||
// So we can directly use innerContainer here.
|
||||
public Building_ArmedShuttle Shuttle => (Building_ArmedShuttle)innerContainer.FirstOrDefault();
|
||||
|
||||
public override Color DrawColor => Shuttle.DrawColor;
|
||||
|
||||
protected override void Impact()
|
||||
{
|
||||
// Re-adding debug logs for stage 6
|
||||
Log.Message($"[WULA] Stage 6: Impact - ArmedShuttleIncoming Impact() called. InnerThing (via innerContainer) is: {innerContainer.FirstOrDefault()?.ToString() ?? "NULL"}");
|
||||
|
||||
Thing innerThing = innerContainer.FirstOrDefault();
|
||||
if (innerThing is Building_ArmedShuttle shuttle)
|
||||
{
|
||||
Log.Message("[WULA] Stage 6: Impact - InnerThing is a Building_ArmedShuttle. Attempting to notify arrival.");
|
||||
shuttle.TryGetComp<CompLaunchable>()?.Notify_Arrived();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WULA] Stage 6: Impact - InnerThing is NOT a Building_ArmedShuttle or is NULL. Type: {innerThing?.GetType().Name ?? "NULL"}. This is the cause of the issue.");
|
||||
}
|
||||
|
||||
// Calling base.Impact() will handle the actual spawning of the innerThing.
|
||||
// This is crucial for "unpacking" the shuttle.
|
||||
base.Impact();
|
||||
}
|
||||
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
// Re-adding debug logs for stage 5
|
||||
Log.Message($"[WULA] Stage 5: Landing Sequence - ArmedShuttleIncoming spawned. InnerThing (via innerContainer) is: {innerContainer.FirstOrDefault()?.ToString() ?? "NULL"}");
|
||||
if (!respawningAfterLoad && !base.BeingTransportedOnGravship)
|
||||
{
|
||||
angle = GetAngle(0f, base.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Destroy(DestroyMode mode = DestroyMode.Vanish)
|
||||
{
|
||||
if (!hasImpacted)
|
||||
{
|
||||
Log.Error("Destroying armed shuttle skyfaller without ever having impacted"); // Changed log message
|
||||
}
|
||||
base.Destroy(mode);
|
||||
}
|
||||
|
||||
protected override void GetDrawPositionAndRotation(ref Vector3 drawLoc, out float extraRotation)
|
||||
{
|
||||
extraRotation = 0f;
|
||||
angle = GetAngle(base.TimeInAnimation, base.Rotation);
|
||||
switch (base.Rotation.AsInt)
|
||||
{
|
||||
case 1:
|
||||
extraRotation += def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
case 3:
|
||||
extraRotation -= def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
}
|
||||
drawLoc.z += def.skyfaller.zPositionCurve.Evaluate(base.TimeInAnimation);
|
||||
}
|
||||
|
||||
public override float DrawAngle()
|
||||
{
|
||||
float num = 0f;
|
||||
switch (base.Rotation.AsInt)
|
||||
{
|
||||
case 1:
|
||||
num += def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
case 3:
|
||||
num -= def.skyfaller.rotationCurve.Evaluate(base.TimeInAnimation);
|
||||
break;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private static float GetAngle(float timeInAnimation, Rot4 rotation)
|
||||
{
|
||||
return rotation.AsInt switch
|
||||
{
|
||||
1 => rotation.Opposite.AsAngle + AngleCurve.Evaluate(timeInAnimation),
|
||||
3 => rotation.Opposite.AsAngle - AngleCurve.Evaluate(timeInAnimation),
|
||||
_ => rotation.Opposite.AsAngle,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
[StaticConstructorOnStartup]
|
||||
public class Building_ArmedShuttle : Building_PassengerShuttle, IAttackTarget, IAttackTargetSearcher
|
||||
{
|
||||
// --- TurretTop nested class ---
|
||||
public class TurretTop
|
||||
{
|
||||
private Building_ArmedShuttle parentTurret;
|
||||
private float curRotationInt;
|
||||
private int ticksUntilIdleTurn;
|
||||
private int idleTurnTicksLeft;
|
||||
private bool idleTurnClockwise;
|
||||
|
||||
private const float IdleTurnDegreesPerTick = 0.26f;
|
||||
private const int IdleTurnDuration = 140;
|
||||
private const int IdleTurnIntervalMin = 150;
|
||||
private const int IdleTurnIntervalMax = 350;
|
||||
public static readonly int ArtworkRotation = -90;
|
||||
|
||||
public float CurRotation
|
||||
{
|
||||
get => curRotationInt;
|
||||
set
|
||||
{
|
||||
curRotationInt = value % 360f;
|
||||
if (curRotationInt < 0f) curRotationInt += 360f;
|
||||
}
|
||||
}
|
||||
|
||||
public TurretTop(Building_ArmedShuttle ParentTurret)
|
||||
{
|
||||
this.parentTurret = ParentTurret;
|
||||
}
|
||||
|
||||
public void SetRotationFromOrientation() => CurRotation = parentTurret.Rotation.AsAngle;
|
||||
|
||||
public void ForceFaceTarget(LocalTargetInfo targ)
|
||||
{
|
||||
if (targ.IsValid)
|
||||
{
|
||||
CurRotation = (targ.Cell.ToVector3Shifted() - parentTurret.DrawPos).AngleFlat();
|
||||
}
|
||||
}
|
||||
|
||||
public void TurretTopTick()
|
||||
{
|
||||
LocalTargetInfo currentTarget = parentTurret.CurrentTarget;
|
||||
if (currentTarget.IsValid)
|
||||
{
|
||||
CurRotation = (currentTarget.Cell.ToVector3Shifted() - parentTurret.DrawPos).AngleFlat();
|
||||
ticksUntilIdleTurn = Rand.RangeInclusive(150, 350);
|
||||
}
|
||||
else if (ticksUntilIdleTurn > 0)
|
||||
{
|
||||
ticksUntilIdleTurn--;
|
||||
if (ticksUntilIdleTurn == 0)
|
||||
{
|
||||
idleTurnClockwise = Rand.Value < 0.5f;
|
||||
idleTurnTicksLeft = 140;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CurRotation += idleTurnClockwise ? 0.26f : -0.26f;
|
||||
idleTurnTicksLeft--;
|
||||
if (idleTurnTicksLeft <= 0)
|
||||
{
|
||||
ticksUntilIdleTurn = Rand.RangeInclusive(150, 350);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawTurret()
|
||||
{
|
||||
Vector3 v = new Vector3(parentTurret.def.building.turretTopOffset.x, 0f, parentTurret.def.building.turretTopOffset.y).RotatedBy(CurRotation);
|
||||
float turretTopDrawSize = parentTurret.def.building.turretTopDrawSize;
|
||||
float num = parentTurret.AttackVerb?.AimAngleOverride ?? CurRotation;
|
||||
Vector3 pos = parentTurret.DrawPos + Altitudes.AltIncVect + v;
|
||||
Quaternion q = ((float)ArtworkRotation + num).ToQuat();
|
||||
Graphics.DrawMesh(matrix: Matrix4x4.TRS(pos, q, new Vector3(turretTopDrawSize, 1f, turretTopDrawSize)), mesh: MeshPool.plane10, material: parentTurret.TurretTopMaterial, layer: 0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Fields ---
|
||||
protected LocalTargetInfo forcedTarget = LocalTargetInfo.Invalid;
|
||||
private LocalTargetInfo lastAttackedTarget;
|
||||
private int lastAttackTargetTick;
|
||||
private StunHandler stunner;
|
||||
private bool triedGettingStunner;
|
||||
protected int burstCooldownTicksLeft;
|
||||
protected int burstWarmupTicksLeft;
|
||||
protected LocalTargetInfo currentTargetInt = LocalTargetInfo.Invalid;
|
||||
private bool holdFire;
|
||||
private bool burstActivated;
|
||||
public Thing gun;
|
||||
protected TurretTop top;
|
||||
protected CompPowerTrader powerComp;
|
||||
protected CompCanBeDormant dormantComp;
|
||||
protected CompInitiatable initiatableComp;
|
||||
protected CompMannable mannableComp;
|
||||
protected CompInteractable interactableComp;
|
||||
public CompRefuelable refuelableComp;
|
||||
protected Effecter progressBarEffecter;
|
||||
protected CompMechPowerCell powerCellComp;
|
||||
protected CompHackable hackableComp;
|
||||
|
||||
// --- PROPERTIES ---
|
||||
Thing IAttackTarget.Thing => this;
|
||||
public LocalTargetInfo TargetCurrentlyAimingAt => CurrentTarget;
|
||||
public float TargetPriorityFactor => 1f;
|
||||
public virtual Material TurretTopMaterial => def.building.turretTopMat;
|
||||
protected bool IsStunned
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!triedGettingStunner)
|
||||
{
|
||||
stunner = GetComp<CompStunnable>()?.StunHandler;
|
||||
triedGettingStunner = true;
|
||||
}
|
||||
return stunner != null && stunner.Stunned;
|
||||
}
|
||||
}
|
||||
public Verb CurrentEffectiveVerb => AttackVerb;
|
||||
public LocalTargetInfo LastAttackedTarget => lastAttackedTarget;
|
||||
public int LastAttackTargetTick => lastAttackTargetTick;
|
||||
public LocalTargetInfo ForcedTarget => forcedTarget;
|
||||
public virtual bool IsEverThreat => true;
|
||||
public bool Active => (powerComp == null || powerComp.PowerOn) && (dormantComp == null || dormantComp.Awake) && (initiatableComp == null || initiatableComp.Initiated) && (interactableComp == null || burstActivated) && (powerCellComp == null || !powerCellComp.depleted) && (hackableComp == null || !hackableComp.IsHacked);
|
||||
public CompEquippable GunCompEq => gun.TryGetComp<CompEquippable>();
|
||||
public virtual LocalTargetInfo CurrentTarget => currentTargetInt;
|
||||
private bool WarmingUp => burstWarmupTicksLeft > 0;
|
||||
public virtual Verb AttackVerb => GunCompEq.PrimaryVerb;
|
||||
public bool IsMannable => mannableComp != null;
|
||||
private bool PlayerControlled => (base.Faction == Faction.OfPlayer || MannedByColonist) && !MannedByNonColonist && !IsActivable;
|
||||
protected virtual bool CanSetForcedTarget => (mannableComp != null || GetComp<CompForceTargetable>() != null) && PlayerControlled;
|
||||
private bool CanToggleHoldFire => PlayerControlled;
|
||||
private bool IsMortar => def.building.IsMortar;
|
||||
private bool IsMortarOrProjectileFliesOverhead => AttackVerb.ProjectileFliesOverhead() || IsMortar;
|
||||
private bool IsActivable => interactableComp != null;
|
||||
protected virtual bool HideForceTargetGizmo => false;
|
||||
public TurretTop Top => top;
|
||||
private bool CanExtractShell => PlayerControlled && (gun.TryGetComp<CompChangeableProjectile>()?.Loaded ?? false);
|
||||
private bool MannedByColonist => mannableComp != null && mannableComp.ManningPawn != null && mannableComp.ManningPawn.Faction == Faction.OfPlayer;
|
||||
private bool MannedByNonColonist => mannableComp != null && mannableComp.ManningPawn != null && mannableComp.ManningPawn.Faction != Faction.OfPlayer;
|
||||
Thing IAttackTargetSearcher.Thing => this;
|
||||
|
||||
// --- CONSTRUCTOR ---
|
||||
public Building_ArmedShuttle()
|
||||
{
|
||||
top = new TurretTop(this);
|
||||
}
|
||||
|
||||
// --- METHODS ---
|
||||
public override void SpawnSetup(Map map, bool respawningAfterLoad)
|
||||
{
|
||||
base.SpawnSetup(map, respawningAfterLoad);
|
||||
dormantComp = GetComp<CompCanBeDormant>();
|
||||
initiatableComp = GetComp<CompInitiatable>();
|
||||
powerComp = GetComp<CompPowerTrader>();
|
||||
mannableComp = GetComp<CompMannable>();
|
||||
interactableComp = GetComp<CompInteractable>();
|
||||
refuelableComp = GetComp<CompRefuelable>();
|
||||
powerCellComp = GetComp<CompMechPowerCell>();
|
||||
hackableComp = GetComp<CompHackable>();
|
||||
if (!respawningAfterLoad)
|
||||
{
|
||||
top.SetRotationFromOrientation();
|
||||
// ShuttleComp.shipParent.Start(); // Already handled by base.SpawnSetup
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostMake()
|
||||
{
|
||||
base.PostMake();
|
||||
burstCooldownTicksLeft = def.building.turretInitialCooldownTime.SecondsToTicks();
|
||||
MakeGun();
|
||||
}
|
||||
|
||||
public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
|
||||
{
|
||||
base.DeSpawn(mode);
|
||||
ResetCurrentTarget();
|
||||
progressBarEffecter?.Cleanup();
|
||||
}
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_TargetInfo.Look(ref forcedTarget, "forcedTarget");
|
||||
Scribe_TargetInfo.Look(ref lastAttackedTarget, "lastAttackedTarget");
|
||||
Scribe_Values.Look(ref lastAttackTargetTick, "lastAttackTargetTick", 0);
|
||||
Scribe_Values.Look(ref burstCooldownTicksLeft, "burstCooldownTicksLeft", 0);
|
||||
Scribe_Values.Look(ref burstWarmupTicksLeft, "burstWarmupTicksLeft", 0);
|
||||
Scribe_TargetInfo.Look(ref currentTargetInt, "currentTarget");
|
||||
Scribe_Values.Look(ref holdFire, "holdFire", defaultValue: false);
|
||||
Scribe_Values.Look(ref burstActivated, "burstActivated", defaultValue: false);
|
||||
Scribe_Deep.Look(ref gun, "gun");
|
||||
// Scribe_Values.Look(ref shuttleName, "shuttleName"); // Already handled by base.ExposeData
|
||||
if (Scribe.mode == LoadSaveMode.PostLoadInit)
|
||||
{
|
||||
if (gun == null)
|
||||
{
|
||||
Log.Error("Turret had null gun after loading. Recreating.");
|
||||
MakeGun();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateGunVerbs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Tick()
|
||||
{
|
||||
base.Tick();
|
||||
if (forcedTarget.HasThing && (!forcedTarget.Thing.Spawned || !base.Spawned || forcedTarget.Thing.Map != base.Map))
|
||||
{
|
||||
forcedTarget = LocalTargetInfo.Invalid;
|
||||
}
|
||||
if (CanExtractShell && MannedByColonist)
|
||||
{
|
||||
CompChangeableProjectile compChangeableProjectile = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (!compChangeableProjectile.allowedShellsSettings.AllowedToAccept(compChangeableProjectile.LoadedShell))
|
||||
{
|
||||
ExtractShell();
|
||||
}
|
||||
}
|
||||
if (forcedTarget.IsValid && !CanSetForcedTarget) ResetForcedTarget();
|
||||
if (!CanToggleHoldFire) holdFire = false;
|
||||
if (forcedTarget.ThingDestroyed) ResetForcedTarget();
|
||||
|
||||
if (Active && (mannableComp == null || mannableComp.MannedNow) && !IsStunned && base.Spawned)
|
||||
{
|
||||
GunCompEq.verbTracker.VerbsTick();
|
||||
if (AttackVerb.state != VerbState.Bursting)
|
||||
{
|
||||
burstActivated = false;
|
||||
if (WarmingUp)
|
||||
{
|
||||
burstWarmupTicksLeft--;
|
||||
if (burstWarmupTicksLeft <= 0) BeginBurst();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (burstCooldownTicksLeft > 0)
|
||||
{
|
||||
burstCooldownTicksLeft--;
|
||||
if (IsMortar)
|
||||
{
|
||||
if (progressBarEffecter == null) progressBarEffecter = EffecterDefOf.ProgressBar.Spawn();
|
||||
progressBarEffecter.EffectTick(this, TargetInfo.Invalid);
|
||||
MoteProgressBar mote = ((SubEffecter_ProgressBar)progressBarEffecter.children[0]).mote;
|
||||
mote.progress = 1f - (float)Mathf.Max(burstCooldownTicksLeft, 0) / (float)BurstCooldownTime().SecondsToTicks();
|
||||
mote.offsetZ = -0.8f;
|
||||
}
|
||||
}
|
||||
if (burstCooldownTicksLeft <= 0 && this.IsHashIntervalTick(15))
|
||||
{
|
||||
TryStartShootSomething(canBeginBurstImmediately: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
top.TurretTopTick();
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<Gizmo> GetGizmos()
|
||||
{
|
||||
foreach (Gizmo gizmo in base.GetGizmos()) yield return gizmo;
|
||||
if (CanExtractShell)
|
||||
{
|
||||
CompChangeableProjectile compChangeableProjectile = gun.TryGetComp<CompChangeableProjectile>();
|
||||
Command_Action command_Action = new Command_Action();
|
||||
command_Action.defaultLabel = "CommandExtractShell".Translate();
|
||||
command_Action.defaultDesc = "CommandExtractShellDesc".Translate();
|
||||
command_Action.icon = compChangeableProjectile.LoadedShell.uiIcon;
|
||||
command_Action.iconAngle = compChangeableProjectile.LoadedShell.uiIconAngle;
|
||||
command_Action.iconOffset = compChangeableProjectile.LoadedShell.uiIconOffset;
|
||||
command_Action.iconDrawScale = GenUI.IconDrawScale(compChangeableProjectile.LoadedShell);
|
||||
command_Action.action = delegate { ExtractShell(); };
|
||||
yield return command_Action;
|
||||
}
|
||||
CompChangeableProjectile compChangeableProjectile2 = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (compChangeableProjectile2 != null)
|
||||
{
|
||||
foreach (Gizmo item in StorageSettingsClipboard.CopyPasteGizmosFor(compChangeableProjectile2.GetStoreSettings()))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
if (!HideForceTargetGizmo)
|
||||
{
|
||||
if (CanSetForcedTarget)
|
||||
{
|
||||
Command_VerbTarget command_VerbTarget = new Command_VerbTarget();
|
||||
command_VerbTarget.defaultLabel = "CommandSetForceAttackTarget".Translate();
|
||||
command_VerbTarget.defaultDesc = "CommandSetForceAttackTargetDesc".Translate();
|
||||
command_VerbTarget.icon = ContentFinder<Texture2D>.Get("UI/Commands/Attack");
|
||||
command_VerbTarget.verb = AttackVerb;
|
||||
command_VerbTarget.hotKey = KeyBindingDefOf.Misc4;
|
||||
command_VerbTarget.drawRadius = false;
|
||||
command_VerbTarget.requiresAvailableVerb = false;
|
||||
if (base.Spawned && IsMortarOrProjectileFliesOverhead && base.Position.Roofed(base.Map))
|
||||
{
|
||||
command_VerbTarget.Disable("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
|
||||
}
|
||||
yield return command_VerbTarget;
|
||||
}
|
||||
if (forcedTarget.IsValid)
|
||||
{
|
||||
Command_Action command_Action2 = new Command_Action();
|
||||
command_Action2.defaultLabel = "CommandStopForceAttack".Translate();
|
||||
command_Action2.defaultDesc = "CommandStopForceAttackDesc".Translate();
|
||||
command_Action2.icon = ContentFinder<Texture2D>.Get("UI/Commands/Halt");
|
||||
command_Action2.action = delegate
|
||||
{
|
||||
ResetForcedTarget();
|
||||
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
|
||||
};
|
||||
if (!forcedTarget.IsValid)
|
||||
{
|
||||
command_Action2.Disable("CommandStopAttackFailNotForceAttacking".Translate());
|
||||
}
|
||||
command_Action2.hotKey = KeyBindingDefOf.Misc5;
|
||||
yield return command_Action2;
|
||||
}
|
||||
}
|
||||
if (CanToggleHoldFire)
|
||||
{
|
||||
Command_Toggle command_Toggle = new Command_Toggle();
|
||||
command_Toggle.defaultLabel = "CommandHoldFire".Translate();
|
||||
command_Toggle.defaultDesc = "CommandHoldFireDesc".Translate();
|
||||
command_Toggle.icon = ContentFinder<Texture2D>.Get("UI/Commands/HoldFire");
|
||||
command_Toggle.hotKey = KeyBindingDefOf.Misc6;
|
||||
command_Toggle.toggleAction = delegate
|
||||
{
|
||||
holdFire = !holdFire;
|
||||
if (holdFire) ResetForcedTarget();
|
||||
};
|
||||
command_Toggle.isActive = () => holdFire;
|
||||
yield return command_Toggle;
|
||||
}
|
||||
// The following gizmos are already provided by Building_PassengerShuttle's GetGizmos()
|
||||
// foreach (Gizmo gizmo in ShuttleComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// foreach (Gizmo gizmo in LaunchableComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// foreach (Gizmo gizmo in TransporterComp.CompGetGizmosExtra()) yield return gizmo;
|
||||
// fuel related gizmos are also handled by base class.
|
||||
}
|
||||
|
||||
public void OrderAttack(LocalTargetInfo targ)
|
||||
{
|
||||
if (!targ.IsValid)
|
||||
{
|
||||
if (forcedTarget.IsValid) ResetForcedTarget();
|
||||
return;
|
||||
}
|
||||
if ((targ.Cell - base.Position).LengthHorizontal < AttackVerb.verbProps.EffectiveMinRange(targ, this))
|
||||
{
|
||||
Messages.Message("MessageTargetBelowMinimumRange".Translate(), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
return;
|
||||
}
|
||||
if ((targ.Cell - base.Position).LengthHorizontal > AttackVerb.EffectiveRange)
|
||||
{
|
||||
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
return;
|
||||
}
|
||||
if (forcedTarget != targ)
|
||||
{
|
||||
forcedTarget = targ;
|
||||
if (burstCooldownTicksLeft <= 0) TryStartShootSomething(canBeginBurstImmediately: false);
|
||||
}
|
||||
if (holdFire)
|
||||
{
|
||||
Messages.Message("MessageTurretWontFireBecauseHoldFire".Translate(def.label), this, MessageTypeDefOf.RejectInput, historical: false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ThreatDisabled(IAttackTargetSearcher disabledFor)
|
||||
{
|
||||
if (!IsEverThreat) return true;
|
||||
if (powerComp != null && !powerComp.PowerOn) return true;
|
||||
if (mannableComp != null && !mannableComp.MannedNow) return true;
|
||||
if (dormantComp != null && !dormantComp.Awake) return true;
|
||||
if (initiatableComp != null && !initiatableComp.Initiated) return true;
|
||||
if (powerCellComp != null && powerCellComp.depleted) return true;
|
||||
if (hackableComp != null && hackableComp.IsHacked) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void OnAttackedTarget(LocalTargetInfo target)
|
||||
{
|
||||
lastAttackTargetTick = Find.TickManager.TicksGame;
|
||||
lastAttackedTarget = target;
|
||||
}
|
||||
|
||||
public void TryStartShootSomething(bool canBeginBurstImmediately)
|
||||
{
|
||||
if (progressBarEffecter != null)
|
||||
{
|
||||
progressBarEffecter.Cleanup();
|
||||
progressBarEffecter = null;
|
||||
}
|
||||
if (!base.Spawned || (holdFire && CanToggleHoldFire) || (AttackVerb.ProjectileFliesOverhead() && base.Map.roofGrid.Roofed(base.Position)) || !AttackVerb.Available())
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
return;
|
||||
}
|
||||
bool wasValid = currentTargetInt.IsValid;
|
||||
currentTargetInt = forcedTarget.IsValid ? forcedTarget : TryFindNewTarget();
|
||||
if (!wasValid && currentTargetInt.IsValid && def.building.playTargetAcquiredSound)
|
||||
{
|
||||
SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(base.Position, base.Map));
|
||||
}
|
||||
if (currentTargetInt.IsValid)
|
||||
{
|
||||
float warmupTime = def.building.turretBurstWarmupTime.RandomInRange;
|
||||
if (warmupTime > 0f)
|
||||
{
|
||||
burstWarmupTicksLeft = warmupTime.SecondsToTicks();
|
||||
}
|
||||
else if (canBeginBurstImmediately)
|
||||
{
|
||||
BeginBurst();
|
||||
}
|
||||
else
|
||||
{
|
||||
burstWarmupTicksLeft = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResetCurrentTarget();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual LocalTargetInfo TryFindNewTarget()
|
||||
{
|
||||
IAttackTargetSearcher searcher = this;
|
||||
Faction faction = searcher.Thing.Faction;
|
||||
float range = AttackVerb.EffectiveRange;
|
||||
if (Rand.Value < 0.5f && AttackVerb.ProjectileFliesOverhead() && faction.HostileTo(Faction.OfPlayer))
|
||||
{
|
||||
if (base.Map.listerBuildings.allBuildingsColonist.Where(delegate(Building x)
|
||||
{
|
||||
float minRange = AttackVerb.verbProps.EffectiveMinRange(x, this);
|
||||
float distSq = x.Position.DistanceToSquared(base.Position);
|
||||
return distSq > minRange * minRange && distSq < range * range;
|
||||
}).TryRandomElement(out Building result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
TargetScanFlags flags = TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable;
|
||||
if (!AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
flags |= TargetScanFlags.NeedLOSToAll | TargetScanFlags.LOSBlockableByGas;
|
||||
}
|
||||
if (AttackVerb.IsIncendiary_Ranged())
|
||||
{
|
||||
flags |= TargetScanFlags.NeedNonBurning;
|
||||
}
|
||||
if (IsMortar)
|
||||
{
|
||||
flags |= TargetScanFlags.NeedNotUnderThickRoof;
|
||||
}
|
||||
return (Thing)AttackTargetFinder.BestShootTargetFromCurrentPosition(searcher, flags, IsValidTarget);
|
||||
}
|
||||
|
||||
private IAttackTargetSearcher TargSearcher() => (mannableComp != null && mannableComp.MannedNow) ? (IAttackTargetSearcher)mannableComp.ManningPawn : this;
|
||||
|
||||
private bool IsValidTarget(Thing t)
|
||||
{
|
||||
if (t is Pawn pawn)
|
||||
{
|
||||
if (base.Faction == Faction.OfPlayer && pawn.IsPrisoner) return false;
|
||||
if (AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
RoofDef roof = base.Map.roofGrid.RoofAt(t.Position);
|
||||
if (roof != null && roof.isThickRoof) return false;
|
||||
}
|
||||
if (mannableComp == null) return !GenAI.MachinesLike(base.Faction, pawn);
|
||||
if (pawn.RaceProps.Animal && pawn.Faction == Faction.OfPlayer) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void BeginBurst()
|
||||
{
|
||||
AttackVerb.TryStartCastOn(CurrentTarget);
|
||||
OnAttackedTarget(CurrentTarget);
|
||||
}
|
||||
|
||||
protected void BurstComplete()
|
||||
{
|
||||
burstCooldownTicksLeft = BurstCooldownTime().SecondsToTicks();
|
||||
}
|
||||
|
||||
protected float BurstCooldownTime() => (def.building.turretBurstCooldownTime >= 0f) ? def.building.turretBurstCooldownTime : AttackVerb.verbProps.defaultCooldownTime;
|
||||
|
||||
public override string GetInspectString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder(base.GetInspectString());
|
||||
if (AttackVerb.verbProps.minRange > 0f)
|
||||
{
|
||||
sb.AppendLine("MinimumRange".Translate() + ": " + AttackVerb.verbProps.minRange.ToString("F0"));
|
||||
}
|
||||
if (base.Spawned && IsMortarOrProjectileFliesOverhead && base.Position.Roofed(base.Map))
|
||||
{
|
||||
sb.AppendLine("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
|
||||
}
|
||||
else if (base.Spawned && burstCooldownTicksLeft > 0 && BurstCooldownTime() > 5f)
|
||||
{
|
||||
sb.AppendLine("CanFireIn".Translate() + ": " + burstCooldownTicksLeft.ToStringSecondsFromTicks());
|
||||
}
|
||||
CompChangeableProjectile changeable = gun.TryGetComp<CompChangeableProjectile>();
|
||||
if (changeable != null)
|
||||
{
|
||||
sb.AppendLine(changeable.Loaded ? "ShellLoaded".Translate(changeable.LoadedShell.LabelCap, changeable.LoadedShell) : "ShellNotLoaded".Translate());
|
||||
}
|
||||
return sb.ToString().TrimEndNewlines();
|
||||
}
|
||||
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
top.DrawTurret();
|
||||
base.DrawAt(drawLoc, flip);
|
||||
}
|
||||
|
||||
public override void DrawExtraSelectionOverlays()
|
||||
{
|
||||
base.DrawExtraSelectionOverlays();
|
||||
float range = AttackVerb.EffectiveRange;
|
||||
if (range < 90f) GenDraw.DrawRadiusRing(base.Position, range);
|
||||
float minRange = AttackVerb.verbProps.EffectiveMinRange(allowAdjacentShot: true);
|
||||
if (minRange < 90f && minRange > 0.1f) GenDraw.DrawRadiusRing(base.Position, minRange);
|
||||
if (WarmingUp)
|
||||
{
|
||||
int degrees = (int)(burstWarmupTicksLeft * 0.5f);
|
||||
GenDraw.DrawAimPie(this, CurrentTarget, degrees, (float)def.size.x * 0.5f);
|
||||
}
|
||||
if (forcedTarget.IsValid && (!forcedTarget.HasThing || forcedTarget.Thing.Spawned))
|
||||
{
|
||||
Vector3 b = forcedTarget.HasThing ? forcedTarget.Thing.TrueCenter() : forcedTarget.Cell.ToVector3Shifted();
|
||||
Vector3 a = this.TrueCenter();
|
||||
b.y = a.y = AltitudeLayer.MetaOverlays.AltitudeFor();
|
||||
GenDraw.DrawLineBetween(a, b, MaterialPool.MatFrom(GenDraw.LineTexPath, ShaderDatabase.Transparent, new Color(1f, 0.5f, 0.5f)));
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractShell() => GenPlace.TryPlaceThing(gun.TryGetComp<CompChangeableProjectile>().RemoveShell(), base.Position, base.Map, ThingPlaceMode.Near);
|
||||
|
||||
private void ResetForcedTarget()
|
||||
{
|
||||
forcedTarget = LocalTargetInfo.Invalid;
|
||||
burstWarmupTicksLeft = 0;
|
||||
if (burstCooldownTicksLeft <= 0) TryStartShootSomething(canBeginBurstImmediately: false);
|
||||
}
|
||||
|
||||
private void ResetCurrentTarget()
|
||||
{
|
||||
currentTargetInt = LocalTargetInfo.Invalid;
|
||||
burstWarmupTicksLeft = 0;
|
||||
}
|
||||
|
||||
public void MakeGun()
|
||||
{
|
||||
gun = ThingMaker.MakeThing(def.building.turretGunDef);
|
||||
UpdateGunVerbs();
|
||||
}
|
||||
|
||||
private void UpdateGunVerbs()
|
||||
{
|
||||
List<Verb> allVerbs = gun.TryGetComp<CompEquippable>().AllVerbs;
|
||||
for (int i = 0; i < allVerbs.Count; i++)
|
||||
{
|
||||
allVerbs[i].caster = this;
|
||||
allVerbs[i].castCompleteCallback = BurstComplete;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using System.Reflection;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 口袋空间退出点建筑 - 继承自MapPortal以获得完整的双向传送功能
|
||||
/// </summary>
|
||||
public class Building_PocketMapExit : MapPortal
|
||||
{
|
||||
/// <summary>目标地图</summary>
|
||||
public Map targetMap;
|
||||
|
||||
/// <summary>目标位置</summary>
|
||||
public IntVec3 targetPos;
|
||||
|
||||
/// <summary>父穿梭机</summary>
|
||||
public Building_ArmedShuttleWithPocket parentShuttle;
|
||||
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_References.Look(ref targetMap, "targetMap");
|
||||
Scribe_Values.Look(ref targetPos, "targetPos");
|
||||
Scribe_References.Look(ref parentShuttle, "parentShuttle");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写获取其他地图,返回主地图(模仿原版MapPortal.GetOtherMap)
|
||||
/// </summary>
|
||||
public override Map GetOtherMap()
|
||||
{
|
||||
// 动态更新目标地图,处理穿梭机移动的情况
|
||||
UpdateTargetFromParentShuttle();
|
||||
return targetMap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写获取目标位置,返回主地图上的穿梭机位置(模仿原版MapPortal.GetDestinationLocation)
|
||||
/// </summary>
|
||||
public override IntVec3 GetDestinationLocation()
|
||||
{
|
||||
// 动态更新目标位置,处理穿梭机移动的情况
|
||||
UpdateTargetFromParentShuttle();
|
||||
return targetPos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从父穿梭机动态更新目标位置,处理穿梭机移动的情况
|
||||
/// </summary>
|
||||
private void UpdateTargetFromParentShuttle()
|
||||
{
|
||||
if (parentShuttle != null && parentShuttle.Spawned)
|
||||
{
|
||||
// 如果穿梭机还在地图上,更新目标位置
|
||||
if (targetMap != parentShuttle.Map || targetPos != parentShuttle.Position)
|
||||
{
|
||||
targetMap = parentShuttle.Map;
|
||||
targetPos = parentShuttle.Position;
|
||||
Log.Message($"[WULA] Updated exit target to shuttle location: {targetMap?.uniqueID} at {targetPos}");
|
||||
}
|
||||
}
|
||||
else if (parentShuttle != null && !parentShuttle.Spawned)
|
||||
{
|
||||
// 穿梭机不在地图上(可能在飞行中)
|
||||
// 保持原有目标,但记录警告
|
||||
if (this.IsHashIntervalTick(2500)) // 每隔一段时间检查一次
|
||||
{
|
||||
Log.Warning($"[WULA] Parent shuttle is not spawned, exit target may be outdated. Last known: {targetMap?.uniqueID} at {targetPos}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写是否可进入,检查目标地图是否存在及传送状态(模仿原版MapPortal.IsEnterable)
|
||||
/// </summary>
|
||||
public override bool IsEnterable(out string reason)
|
||||
{
|
||||
if (targetMap == null)
|
||||
{
|
||||
reason = "WULA.PocketSpace.NoTargetMap".Translate();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查父穿梭机的传送状态
|
||||
if (parentShuttle != null)
|
||||
{
|
||||
// 使用反射获取 transportDisabled 字段值
|
||||
var transportDisabledField = typeof(Building_ArmedShuttleWithPocket).GetField("transportDisabled",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
if (transportDisabledField != null)
|
||||
{
|
||||
bool transportDisabled = (bool)transportDisabledField.GetValue(parentShuttle);
|
||||
if (transportDisabled)
|
||||
{
|
||||
reason = "WULA.PocketSpace.TransportDisabled".Translate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reason = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入事件,处理从口袋空间退出到主地图(模仿原版MapPortal.OnEntered)
|
||||
/// </summary>
|
||||
public override void OnEntered(Pawn pawn)
|
||||
{
|
||||
// 不调用 base.OnEntered,因为我们不需要原版的通知机制
|
||||
// 直接处理退出逻辑
|
||||
if (targetMap != null && pawn.Spawned)
|
||||
{
|
||||
ExitPocketSpace(pawn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入按钮文本
|
||||
/// </summary>
|
||||
public override string EnterString => "WULA.PocketSpace.ExitToMainMap".Translate();
|
||||
|
||||
/// <summary>
|
||||
/// 重写进入按钮图标,使用装载按钮的贴图
|
||||
/// </summary>
|
||||
protected override Texture2D EnterTex => ContentFinder<Texture2D>.Get("UI/Commands/LoadTransporter");
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 单个人员退出口袋空间(简化版本,利用MapPortal功能)
|
||||
/// </summary>
|
||||
private void ExitPocketSpace(Pawn pawn)
|
||||
{
|
||||
if (targetMap == null || !pawn.Spawned) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 在目标地图找一个安全位置
|
||||
IntVec3 exitPos = CellFinder.RandomClosewalkCellNear(targetPos, targetMap, 3, p => p.Standable(targetMap));
|
||||
|
||||
// 传送人员
|
||||
pawn.DeSpawn();
|
||||
GenPlace.TryPlaceThing(pawn, exitPos, targetMap, ThingPlaceMode.Near);
|
||||
|
||||
// 切换到主地图
|
||||
if (pawn.IsColonistPlayerControlled)
|
||||
{
|
||||
Current.Game.CurrentMap = targetMap;
|
||||
Find.CameraDriver.JumpToCurrentMapLoc(exitPos);
|
||||
}
|
||||
|
||||
Messages.Message("WULA.PocketSpace.ExitSuccess".Translate(pawn.LabelShort), MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error exiting pocket space: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using RimWorld.Planet;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class Dialog_ArmedShuttleTransfer : Window
|
||||
{
|
||||
private enum Tab
|
||||
{
|
||||
Pawns,
|
||||
Items
|
||||
}
|
||||
|
||||
private const float TitleRectHeight = 35f;
|
||||
private const float BottomAreaHeight = 55f;
|
||||
private readonly Vector2 BottomButtonSize = new Vector2(160f, 40f);
|
||||
|
||||
private Building_ArmedShuttleWithPocket shuttle;
|
||||
private List<TransferableOneWay> transferables;
|
||||
private TransferableOneWayWidget pawnsTransfer;
|
||||
private TransferableOneWayWidget itemsTransfer;
|
||||
private Tab tab;
|
||||
|
||||
private static List<TabRecord> tabsList = new List<TabRecord>();
|
||||
|
||||
public override Vector2 InitialSize => new Vector2(1024f, UI.screenHeight);
|
||||
protected override float Margin => 0f;
|
||||
|
||||
public Dialog_ArmedShuttleTransfer(Building_ArmedShuttleWithPocket shuttle)
|
||||
{
|
||||
this.shuttle = shuttle;
|
||||
forcePause = true;
|
||||
absorbInputAroundWindow = true;
|
||||
}
|
||||
|
||||
public override void PostOpen()
|
||||
{
|
||||
base.PostOpen();
|
||||
CalculateAndRecacheTransferables();
|
||||
}
|
||||
|
||||
public override void DoWindowContents(Rect inRect)
|
||||
{
|
||||
Rect rect = new Rect(0f, 0f, inRect.width, TitleRectHeight);
|
||||
using (new TextBlock(GameFont.Medium, TextAnchor.MiddleCenter))
|
||||
{
|
||||
Widgets.Label(rect, shuttle.EnterString);
|
||||
}
|
||||
|
||||
tabsList.Clear();
|
||||
tabsList.Add(new TabRecord("PawnsTab".Translate(), delegate
|
||||
{
|
||||
tab = Tab.Pawns;
|
||||
}, tab == Tab.Pawns));
|
||||
tabsList.Add(new TabRecord("ItemsTab".Translate(), delegate
|
||||
{
|
||||
tab = Tab.Items;
|
||||
}, tab == Tab.Items));
|
||||
|
||||
inRect.yMin += 67f;
|
||||
Widgets.DrawMenuSection(inRect);
|
||||
TabDrawer.DrawTabs(inRect, tabsList);
|
||||
inRect = inRect.ContractedBy(17f);
|
||||
|
||||
Widgets.BeginGroup(inRect);
|
||||
Rect rect2 = inRect.AtZero();
|
||||
DoBottomButtons(rect2);
|
||||
Rect inRect2 = rect2;
|
||||
inRect2.yMax -= 76f;
|
||||
|
||||
bool anythingChanged = false;
|
||||
switch (tab)
|
||||
{
|
||||
case Tab.Pawns:
|
||||
pawnsTransfer.OnGUI(inRect2, out anythingChanged);
|
||||
break;
|
||||
case Tab.Items:
|
||||
itemsTransfer.OnGUI(inRect2, out anythingChanged);
|
||||
break;
|
||||
}
|
||||
Widgets.EndGroup();
|
||||
}
|
||||
|
||||
private void DoBottomButtons(Rect rect)
|
||||
{
|
||||
float buttonY = rect.height - BottomAreaHeight - 17f;
|
||||
|
||||
if (Widgets.ButtonText(new Rect(rect.width / 2f - BottomButtonSize.x / 2f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "ResetButton".Translate()))
|
||||
{
|
||||
SoundDefOf.Tick_Low.PlayOneShotOnCamera();
|
||||
CalculateAndRecacheTransferables();
|
||||
}
|
||||
if (Widgets.ButtonText(new Rect(0f, buttonY, BottomButtonSize.x, BottomButtonSize.y), "CancelButton".Translate()))
|
||||
{
|
||||
Close();
|
||||
}
|
||||
if (Widgets.ButtonText(new Rect(rect.width - BottomButtonSize.x, buttonY, BottomButtonSize.x, BottomButtonSize.y), "AcceptButton".Translate()) && TryAccept())
|
||||
{
|
||||
SoundDefOf.Tick_High.PlayOneShotOnCamera();
|
||||
Close(doCloseSound: false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryAccept()
|
||||
{
|
||||
// 获取选中的Pawn和物品
|
||||
List<Pawn> pawnsToTransfer = TransferableUtility.GetPawnsFromTransferables(transferables);
|
||||
List<Thing> itemsToTransfer = new List<Thing>();
|
||||
foreach (TransferableOneWay transferable in transferables)
|
||||
{
|
||||
if (transferable.ThingDef.category != ThingCategory.Pawn)
|
||||
{
|
||||
itemsToTransfer.AddRange(transferable.things.Take(transferable.CountToTransfer));
|
||||
}
|
||||
}
|
||||
|
||||
// 传送Pawn到口袋空间
|
||||
int transferredPawnCount = 0;
|
||||
foreach (Pawn pawn in pawnsToTransfer)
|
||||
{
|
||||
if (shuttle.TransferPawnToPocketSpace(pawn))
|
||||
{
|
||||
transferredPawnCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int transferredItemCount = 0;
|
||||
foreach (Thing item in itemsToTransfer)
|
||||
{
|
||||
// 从当前地图移除物品
|
||||
item.DeSpawn();
|
||||
|
||||
// 尝试放置到口袋空间地上
|
||||
IntVec3 dropPos = CellFinder.RandomClosewalkCellNear(shuttle.PocketMap.Center, shuttle.PocketMap, 5); // 随机位置,避免重叠
|
||||
if (dropPos.IsValid)
|
||||
{
|
||||
GenPlace.TryPlaceThing(item, dropPos, shuttle.PocketMap, ThingPlaceMode.Near);
|
||||
transferredItemCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[WULA-ERROR] Could not find valid drop position for item {item.LabelShort} in pocket map.");
|
||||
item.Destroy(); // 实在没地方放,就销毁
|
||||
}
|
||||
}
|
||||
|
||||
if (transferredPawnCount > 0 || transferredItemCount > 0)
|
||||
{
|
||||
Messages.Message("WULA.PocketSpace.TransferSuccess".Translate(transferredPawnCount + transferredItemCount), MessageTypeDefOf.PositiveEvent);
|
||||
// 切换到口袋地图视角(如果传送了Pawn)
|
||||
if (transferredPawnCount > 0)
|
||||
{
|
||||
Current.Game.CurrentMap = shuttle.PocketMap;
|
||||
Find.CameraDriver.JumpToCurrentMapLoc(shuttle.PocketMap.Center);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Messages.Message("WULA.PocketSpace.NoPawnsOrItemsSelected".Translate(), MessageTypeDefOf.RejectInput);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CalculateAndRecacheTransferables()
|
||||
{
|
||||
transferables = new List<TransferableOneWay>();
|
||||
// 根据需要添加现有物品到transferables(如果穿梭机已有物品)
|
||||
// 目前,我们从头开始构建列表,只添加地图上的物品和Pawn
|
||||
|
||||
AddPawnsToTransferables();
|
||||
AddItemsToTransferables();
|
||||
|
||||
// 重新创建TransferableOneWayWidget实例
|
||||
pawnsTransfer = new TransferableOneWayWidget(null, null, null, "TransferMapPortalColonyThingCountTip".Translate(),
|
||||
drawMass: true,
|
||||
ignorePawnInventoryMass: IgnorePawnsInventoryMode.IgnoreIfAssignedToUnload,
|
||||
includePawnsMassInMassUsage: true,
|
||||
availableMassGetter: () => float.MaxValue,
|
||||
extraHeaderSpace: 0f,
|
||||
ignoreSpawnedCorpseGearAndInventoryMass: false,
|
||||
tile: shuttle.Map.Tile,
|
||||
drawMarketValue: false,
|
||||
drawEquippedWeapon: true);
|
||||
CaravanUIUtility.AddPawnsSections(pawnsTransfer, transferables);
|
||||
|
||||
itemsTransfer = new TransferableOneWayWidget(transferables.Where(x => x.ThingDef.category != ThingCategory.Pawn), null, null, "TransferMapPortalColonyThingCountTip".Translate(),
|
||||
drawMass: true,
|
||||
ignorePawnInventoryMass: IgnorePawnsInventoryMode.IgnoreIfAssignedToUnload,
|
||||
includePawnsMassInMassUsage: true,
|
||||
availableMassGetter: () => float.MaxValue,
|
||||
extraHeaderSpace: 0f,
|
||||
ignoreSpawnedCorpseGearAndInventoryMass: false,
|
||||
tile: shuttle.Map.Tile);
|
||||
}
|
||||
|
||||
private void AddToTransferables(Thing t)
|
||||
{
|
||||
TransferableOneWay transferableOneWay = TransferableUtility.TransferableMatching(t, transferables, TransferAsOneMode.PodsOrCaravanPacking);
|
||||
if (transferableOneWay == null)
|
||||
{
|
||||
transferableOneWay = new TransferableOneWay();
|
||||
transferables.Add(transferableOneWay);
|
||||
}
|
||||
if (transferableOneWay.things.Contains(t))
|
||||
{
|
||||
Log.Error("Tried to add the same thing twice to TransferableOneWay: " + t);
|
||||
}
|
||||
else
|
||||
{
|
||||
transferableOneWay.things.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddPawnsToTransferables()
|
||||
{
|
||||
foreach (Pawn item in CaravanFormingUtility.AllSendablePawns(shuttle.Map, allowEvenIfDowned: true, allowEvenIfInMentalState: false, allowEvenIfPrisonerNotSecure: false, allowCapturableDownedPawns: false, allowLodgers: true))
|
||||
{
|
||||
AddToTransferables(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddItemsToTransferables()
|
||||
{
|
||||
// 考虑是否需要处理口袋地图中的物品
|
||||
bool isPocketMap = shuttle.Map.IsPocketMap;
|
||||
foreach (Thing item in CaravanFormingUtility.AllReachableColonyItems(shuttle.Map, isPocketMap, isPocketMap))
|
||||
{
|
||||
AddToTransferables(item);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAcceptKeyPressed()
|
||||
{
|
||||
if (TryAccept())
|
||||
{
|
||||
SoundDefOf.Tick_High.PlayOneShotOnCamera();
|
||||
Close(doCloseSound: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 13x13小型口袋空间生成器
|
||||
/// 创建一个简单的13x13空间,边缘是墙,中间是空地,适合作为穿梭机内部空间
|
||||
/// </summary>
|
||||
public class GenStep_WulaPocketSpaceSmall : GenStep
|
||||
{
|
||||
public override int SeedPart => 928735; // 不同于AncientStockpile的种子
|
||||
|
||||
// 允许通过XML配置指定要生成的预制件Def名称
|
||||
public string prefabDefName;
|
||||
|
||||
public override void Generate(Map map, GenStepParams parms)
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Message($"[WULA] Generating WULA pocket space, map size: {map.Size}");
|
||||
|
||||
// 获取地图边界
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 生成外围岩石墙壁
|
||||
GenerateWalls(map);
|
||||
|
||||
// 生成内部地板
|
||||
GenerateFloor(map);
|
||||
|
||||
Log.Message("[WULA] WULA pocket space generation completed");
|
||||
|
||||
// 添加预制件生成
|
||||
// 如果指定了预制件Def名称,则加载并生成
|
||||
if (!string.IsNullOrEmpty(prefabDefName))
|
||||
{
|
||||
PrefabDef customPrefabDef = DefDatabase<PrefabDef>.GetNamed(prefabDefName, false);
|
||||
if (customPrefabDef != null)
|
||||
{
|
||||
GeneratePrefab(map, customPrefabDef);
|
||||
Log.Message($"[WULA] Generated custom prefab: {customPrefabDef.defName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"[WULA] Custom prefab '{prefabDefName}' not found. Skipping prefab generation.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error generating WULA pocket space: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成外围墙壁
|
||||
/// </summary>
|
||||
private void GenerateWalls(Map map)
|
||||
{
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 获取地形和物品定义
|
||||
TerrainDef roughTerrain = DefDatabase<TerrainDef>.GetNamed("Granite_Rough", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Granite_Smooth", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Sandstone_Rough", false);
|
||||
|
||||
ThingDef rockWallDef = DefDatabase<ThingDef>.GetNamed("Wall_Rock", false) ??
|
||||
DefDatabase<ThingDef>.GetNamed("Wall", false);
|
||||
|
||||
// 遍历地图边缘,放置WulaWall
|
||||
for (int x = 0; x < mapSize.x; x++)
|
||||
{
|
||||
for (int z = 0; z < mapSize.z; z++)
|
||||
{
|
||||
// 如果是边缘位置,放置WulaWall
|
||||
if (x == 0 || x == mapSize.x - 1 || z == 0 || z == mapSize.z - 1)
|
||||
{
|
||||
IntVec3 pos = new IntVec3(x, 0, z);
|
||||
|
||||
// 设置地形为岩石基础
|
||||
if (roughTerrain != null)
|
||||
{
|
||||
map.terrainGrid.SetTerrain(pos, roughTerrain);
|
||||
}
|
||||
|
||||
// 放置WulaWall
|
||||
ThingDef wallDef = DefDatabase<ThingDef>.GetNamed("WulaWall", false);
|
||||
if (wallDef != null)
|
||||
{
|
||||
// WulaWall是madeFromStuff的建筑,需要指定材料
|
||||
ThingDef steelDef = DefDatabase<ThingDef>.GetNamed("Steel", false);
|
||||
Thing wall = ThingMaker.MakeThing(wallDef, steelDef);
|
||||
wall.SetFaction(null);
|
||||
GenPlace.TryPlaceThing(wall, pos, map, ThingPlaceMode.Direct);
|
||||
}
|
||||
else if (rockWallDef != null)
|
||||
{
|
||||
// 如果WulaWall不存在,使用原版岩石墙作为备选
|
||||
Thing wall = ThingMaker.MakeThing(rockWallDef);
|
||||
wall.SetFaction(null);
|
||||
GenPlace.TryPlaceThing(wall, pos, map, ThingPlaceMode.Direct);
|
||||
Log.Warning("[WULA] WulaWall not found, using fallback wall");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成内部地板
|
||||
/// </summary>
|
||||
private void GenerateFloor(Map map)
|
||||
{
|
||||
IntVec3 mapSize = map.Size;
|
||||
|
||||
// 为内部区域设置WulaFloor
|
||||
TerrainDef floorDef = DefDatabase<TerrainDef>.GetNamed("WulaFloor", false);
|
||||
TerrainDef fallbackFloor = floorDef ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Steel", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("MetalTile", false) ??
|
||||
DefDatabase<TerrainDef>.GetNamed("Concrete", false);
|
||||
|
||||
if (floorDef == null)
|
||||
{
|
||||
Log.Warning("[WULA] WulaFloor not found, using fallback floor");
|
||||
}
|
||||
|
||||
// 清理内部区域并设置正确的地板
|
||||
for (int x = 1; x < mapSize.x - 1; x++)
|
||||
{
|
||||
for (int z = 1; z < mapSize.z - 1; z++)
|
||||
{
|
||||
IntVec3 pos = new IntVec3(x, 0, z);
|
||||
|
||||
// 清理该位置的所有岩石和阻挡物
|
||||
ClearCellAndSetFloor(map, pos, fallbackFloor);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"[WULA] Set floor for internal area ({mapSize.x-2}x{mapSize.z-2}) to {(floorDef?.defName ?? fallbackFloor?.defName)}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理单元格并设置地板
|
||||
/// </summary>
|
||||
private void ClearCellAndSetFloor(Map map, IntVec3 pos, TerrainDef floorDef)
|
||||
{
|
||||
if (!pos.InBounds(map)) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 获取该位置的所有物品
|
||||
List<Thing> thingsAtPos = pos.GetThingList(map).ToList(); // 创建副本避免修改时出错
|
||||
|
||||
// 清理所有建筑物和岩石(强力清理,确保地板可以放置)
|
||||
foreach (Thing thing in thingsAtPos)
|
||||
{
|
||||
bool shouldRemove = false;
|
||||
|
||||
// 检查是否为建筑物
|
||||
if (thing.def.category == ThingCategory.Building)
|
||||
{
|
||||
// 如果是自然岩石
|
||||
if (thing.def.building?.isNaturalRock == true)
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
// 或者是岩石相关的建筑
|
||||
else if (thing.def.defName.Contains("Rock") ||
|
||||
thing.def.defName.Contains("Slate") ||
|
||||
thing.def.defName.Contains("Granite") ||
|
||||
thing.def.defName.Contains("Sandstone") ||
|
||||
thing.def.defName.Contains("Limestone") ||
|
||||
thing.def.defName.Contains("Marble") ||
|
||||
thing.def.defName.Contains("Quartzite") ||
|
||||
thing.def.defName.Contains("Jade"))
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
// 或者是其他阻挡的建筑物(除了我们的乌拉墙)
|
||||
else if (!thing.def.defName.Contains("Wula") && thing.def.Fillage == FillCategory.Full)
|
||||
{
|
||||
shouldRemove = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemove)
|
||||
{
|
||||
if (Prefs.DevMode) // 只在开发模式下输出详细日志
|
||||
{
|
||||
Log.Message($"[WULA] Removing {thing.def.defName} at {pos} to make space for floor");
|
||||
}
|
||||
thing.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
|
||||
// 在清理后稍微延迟,再检查一次(确保彻底清理)
|
||||
thingsAtPos = pos.GetThingList(map).ToList();
|
||||
foreach (Thing thing in thingsAtPos)
|
||||
{
|
||||
if (thing.def.category == ThingCategory.Building && thing.def.Fillage == FillCategory.Full)
|
||||
{
|
||||
Log.Warning($"[WULA] Force removing remaining building {thing.def.defName} at {pos}");
|
||||
thing.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置地板地形
|
||||
if (floorDef != null)
|
||||
{
|
||||
map.terrainGrid.SetTerrain(pos, floorDef);
|
||||
if (Prefs.DevMode)
|
||||
{
|
||||
Log.Message($"[WULA] Set terrain at {pos} to {floorDef.defName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"[WULA] Error clearing cell at {pos}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成预制件
|
||||
/// </summary>
|
||||
private void GeneratePrefab(Map map, PrefabDef prefabDef)
|
||||
{
|
||||
if (prefabDef == null)
|
||||
{
|
||||
Log.Error("[WULA] PrefabDef is null, cannot generate prefab.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取预制件的中心点,将其放置在口袋空间的中心
|
||||
IntVec3 mapCenter = map.Center;
|
||||
IntVec3 prefabOrigin = mapCenter - new IntVec3(prefabDef.size.x / 2, 0, prefabDef.size.z / 2);
|
||||
|
||||
// 生成物品
|
||||
foreach (var thingData in prefabDef.GetThings())
|
||||
{
|
||||
IntVec3 thingPos = prefabOrigin + thingData.cell;
|
||||
if (thingPos.InBounds(map))
|
||||
{
|
||||
Thing thing = ThingMaker.MakeThing(thingData.data.def, thingData.data.stuff);
|
||||
if (thing != null)
|
||||
{
|
||||
// PrefabThingData 不包含 factionDef,派系通常在生成时由上下文决定
|
||||
// thing.SetFaction(thingData.data.factionDef != null ? Faction.OfPlayerSilentFail : null);
|
||||
GenPlace.TryPlaceThing(thing, thingPos, map, ThingPlaceMode.Direct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成地形
|
||||
foreach (var terrainData in prefabDef.GetTerrain())
|
||||
{
|
||||
IntVec3 terrainPos = prefabOrigin + terrainData.cell;
|
||||
if (terrainPos.InBounds(map))
|
||||
{
|
||||
map.terrainGrid.SetTerrain(terrainPos, terrainData.data.def);
|
||||
}
|
||||
}
|
||||
|
||||
// 递归生成子预制件(如果存在)
|
||||
foreach (var subPrefabData in prefabDef.GetPrefabs())
|
||||
{
|
||||
// 这里需要递归调用GeneratePrefab,但为了简化,暂时只处理顶层
|
||||
// 实际项目中,可能需要更复杂的逻辑来处理子预制件的位置和旋转
|
||||
Log.Warning($"[WULA] Sub-prefabs are not fully supported in this simple generator: {subPrefabData.data.def.defName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于武装穿梭机口袋空间的IThingHolder实现,与CompTransporter的容器分离
|
||||
/// </summary>
|
||||
public class PocketSpaceThingHolder : IThingHolder, IExposable
|
||||
{
|
||||
/// <summary>持有的物品容器</summary>
|
||||
public ThingOwner<Thing> innerContainer;
|
||||
|
||||
/// <summary>该容器的拥有者(通常是Building_ArmedShuttleWithPocket)</summary>
|
||||
private IThingHolder owner;
|
||||
|
||||
/// <summary>实现IThingHolder.ParentHolder属性</summary>
|
||||
public IThingHolder ParentHolder => owner;
|
||||
|
||||
public PocketSpaceThingHolder()
|
||||
{
|
||||
innerContainer = new ThingOwner<Thing>(this);
|
||||
}
|
||||
|
||||
public PocketSpaceThingHolder(IThingHolder owner) : this()
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取直接持有的物品
|
||||
/// </summary>
|
||||
public ThingOwner GetDirectlyHeldThings()
|
||||
{
|
||||
return innerContainer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取子持有者
|
||||
/// </summary>
|
||||
public void GetChildHolders(List<IThingHolder> outChildren)
|
||||
{
|
||||
// 目前没有子持有者,留空
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知物品被添加
|
||||
/// </summary>
|
||||
public void Notify_ThingAdded(Thing t)
|
||||
{
|
||||
// 这里可以添加逻辑来处理物品被添加到口袋空间的情况
|
||||
Log.Message($"[WULA] Item {t.LabelCap} added to pocket space container.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通知物品被移除
|
||||
/// </summary>
|
||||
public void Notify_ThingRemoved(Thing t)
|
||||
{
|
||||
// 这里可以添加逻辑来处理物品被从口袋空间移除的情况
|
||||
Log.Message($"[WULA] Item {t.LabelCap} removed from pocket space container.");
|
||||
}
|
||||
|
||||
public void ExposeData()
|
||||
{
|
||||
Scribe_Deep.Look(ref innerContainer, "innerContainer", this);
|
||||
// owner 通常在构造函数中设置,不需要序列化
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompAbilityEffect_GiveSwitchHediff : CompAbilityEffect
|
||||
{
|
||||
public new CompProperties_AbilityGiveHediff Props => (CompProperties_AbilityGiveHediff)props;
|
||||
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
if (Props.hediffDef != null)
|
||||
{
|
||||
parent.pawn.health.AddHediff(Props.hediffDef);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ShouldHideGizmo
|
||||
{
|
||||
get
|
||||
{
|
||||
// 如果父级Pawn已经有了这个Hediff,就隐藏“给予”按钮
|
||||
if (parent.pawn?.health.hediffSet.HasHediff(Props.hediffDef) ?? false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return base.ShouldHideGizmo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompAbilityEffect_RemoveSwitchHediff : CompAbilityEffect
|
||||
{
|
||||
public new CompProperties_AbilityRemoveHediff Props => (CompProperties_AbilityRemoveHediff)props;
|
||||
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
Hediff firstHediffOfDef = parent.pawn.health.hediffSet.GetFirstHediffOfDef(Props.hediffDef);
|
||||
if (firstHediffOfDef != null)
|
||||
{
|
||||
parent.pawn.health.RemoveHediff(firstHediffOfDef);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ShouldHideGizmo
|
||||
{
|
||||
get
|
||||
{
|
||||
// 如果父级Pawn没有这个Hediff,就隐藏“移除”按钮
|
||||
if (!parent.pawn?.health.hediffSet.HasHediff(Props.hediffDef) ?? true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return base.ShouldHideGizmo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using RimWorld;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.Remoting.Messaging;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_Switch : CompProperties_EquippableAbility
|
||||
{
|
||||
public ThingDef changeTo;
|
||||
public CompProperties_Switch()
|
||||
{
|
||||
compClass = typeof(CompSwitch);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompSwitch : CompEquippableAbility
|
||||
{
|
||||
public CompProperties_Switch Props => (CompProperties_Switch)props;
|
||||
|
||||
public HediffWithComps hediff;
|
||||
public HediffComp_Disappears Disappears => hediff.GetComp<HediffComp_Disappears>();
|
||||
public override void Notify_Equipped(Pawn pawn)
|
||||
{
|
||||
base.Notify_Equipped(pawn);
|
||||
if (hediff != null)
|
||||
{
|
||||
pawn.health.AddHediff(hediff);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Notify_Unequipped(Pawn pawn)
|
||||
{
|
||||
base.Notify_Unequipped(pawn);
|
||||
if (hediff != null)
|
||||
{
|
||||
pawn.health.RemoveHediff(hediff);
|
||||
}
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
string text = "";
|
||||
if (hediff != null)
|
||||
{
|
||||
text += hediff.LabelBase + ": " + Disappears.ticksToDisappear.ToStringSecondsFromTicks("F0");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
if (hediff != null)
|
||||
{
|
||||
float severityAdjustment = 0f;
|
||||
Disappears.CompPostTick(ref severityAdjustment);
|
||||
if (Disappears.ticksToDisappear <= 0)
|
||||
{
|
||||
hediff = null;
|
||||
Extension.ChangeOldThing(parent, Props.changeTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_References.Look(ref hediff, "hediff", true);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompAbilityEffect_Switch : CompAbilityEffect
|
||||
{
|
||||
public Pawn Pawn => parent.pawn;
|
||||
public ThingWithComps BaseForm => Pawn.equipment.Primary;
|
||||
public ThingDef ChangeTo => BaseForm.GetComp<CompSwitch>().Props.changeTo;
|
||||
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
Pawn.ChangeEquipThing(BaseForm, ChangeTo);
|
||||
}
|
||||
public override bool AICanTargetNow(LocalTargetInfo target)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class CompAbilityEffect_RemoveHediff : CompAbilityEffect
|
||||
{
|
||||
public Pawn Pawn => parent.pawn;
|
||||
public ThingWithComps BaseForm => Pawn.equipment.Primary;
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
CompSwitch comp = BaseForm.GetComp<CompSwitch>();
|
||||
if (comp.hediff != null)
|
||||
{
|
||||
comp.Disappears.ticksToDisappear = 0;
|
||||
Pawn.health.RemoveHediff(comp.hediff);
|
||||
comp.hediff = null;
|
||||
}
|
||||
}
|
||||
public override bool AICanTargetNow(LocalTargetInfo target)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Extension
|
||||
{
|
||||
public static ThingWithComps ChangeThing(ThingWithComps baseForm, ThingDef changeTo)
|
||||
{
|
||||
int hitPoints = baseForm.HitPoints;
|
||||
ThingDef stuff = null;
|
||||
if (baseForm.Stuff != null)
|
||||
{
|
||||
stuff = baseForm.Stuff;
|
||||
}
|
||||
ThingWithComps newThing = (ThingWithComps)ThingMaker.MakeThing(changeTo, stuff);
|
||||
newThing.HitPoints = hitPoints;
|
||||
for (int i = 0; i < newThing.AllComps.Count; i++)
|
||||
{
|
||||
CompProperties Index = newThing.AllComps[i].props;
|
||||
ThingComp baseComp = baseForm.GetCompByDefType(Index);
|
||||
if (baseComp != null)
|
||||
{
|
||||
baseComp.parent = newThing;
|
||||
newThing.AllComps[i] = baseComp;
|
||||
}
|
||||
}
|
||||
CompSwitch compSwitch = newThing.GetComp<CompSwitch>();
|
||||
compSwitch.Initialize(newThing.def.comps.Find(x => x.compClass == typeof(CompSwitch)));
|
||||
ThingStyleDef styleDef = baseForm.StyleDef;
|
||||
if (baseForm.def.randomStyle != null && newThing.def.randomStyle != null)
|
||||
{
|
||||
ThingStyleChance chance = baseForm.def.randomStyle.Find(x => x.StyleDef == styleDef);
|
||||
int index = baseForm.def.randomStyle.IndexOf(chance);
|
||||
newThing.StyleDef = newThing.def.randomStyle[index].StyleDef;
|
||||
}
|
||||
return newThing;
|
||||
}
|
||||
|
||||
public static void ChangeOldThing(ThingWithComps baseForm, ThingDef changeTo)
|
||||
{
|
||||
ThingWithComps newThing = ChangeThing(baseForm, changeTo);
|
||||
IntVec3 intVec3 = baseForm.Position;
|
||||
Map map = baseForm.Map;
|
||||
baseForm.Destroy();
|
||||
GenSpawn.Spawn(newThing, intVec3, map);
|
||||
}
|
||||
|
||||
public static void ChangeEquipThing(this Pawn pawn, ThingWithComps baseForm, ThingDef changeTo)
|
||||
{
|
||||
ThingWithComps newThing = ChangeThing(baseForm, changeTo);
|
||||
pawn.equipment.DestroyEquipment(baseForm);
|
||||
baseForm.Notify_Unequipped(pawn);
|
||||
pawn.equipment.MakeRoomFor(newThing);
|
||||
pawn.equipment.AddEquipment(newThing);
|
||||
}
|
||||
}
|
||||
|
||||
public class HediffCompPropertiesSwitch : HediffCompProperties_GiveAbility
|
||||
{
|
||||
public HediffCompPropertiesSwitch()
|
||||
{
|
||||
compClass = typeof(SwitchWhenDisappear);
|
||||
}
|
||||
}
|
||||
public class SwitchWhenDisappear : HediffComp_GiveAbility
|
||||
{
|
||||
public HediffCompPropertiesSwitch Props => (HediffCompPropertiesSwitch)props;
|
||||
public override void CompPostMake()
|
||||
{
|
||||
base.CompPostMake();
|
||||
CompSwitch compSwitch = Pawn.equipment.Primary.GetComp<CompSwitch>();
|
||||
compSwitch.hediff = parent;
|
||||
}
|
||||
public override void CompPostPostRemoved()
|
||||
{
|
||||
base.CompPostPostRemoved();
|
||||
if (parent.ShouldRemove)
|
||||
{
|
||||
ThingWithComps thing = Pawn.equipment.Primary;
|
||||
if (thing != null)
|
||||
{
|
||||
CompSwitch compSwitch = thing.GetComp<CompSwitch>();
|
||||
compSwitch.hediff = null;
|
||||
if (compSwitch != null)
|
||||
{
|
||||
Pawn.ChangeEquipThing(thing, compSwitch.Props.changeTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Verse;
|
||||
using RimWorld;
|
||||
using System.Collections.Generic; // Added for List
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompUseEffect_FixAllHealthConditions : CompUseEffect
|
||||
{
|
||||
public override AcceptanceReport CanBeUsedBy(Pawn p)
|
||||
{
|
||||
// 检查是否有可修复的健康状况。
|
||||
// 注意:这里的 'p' 是尝试使用物品的小人(如果直接使用)或被施用物品的小人(如果通过配方施用)。
|
||||
// 种族检查将在 DoEffect 中进行,以允许任何种族的小人尝试使用(但只有乌拉族会生效)。
|
||||
if (!HealthUtility.TryGetWorstHealthCondition(p, out var _, out var _))
|
||||
{
|
||||
return "AbilityCannotCastNoHealableInjury".Translate(p.Named("PAWN")).Resolve().StripTags() ?? "";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void DoEffect(Pawn usedBy)
|
||||
{
|
||||
base.DoEffect(usedBy);
|
||||
|
||||
// 检查被施用物品的小人是否是乌拉族。
|
||||
if (usedBy.def.defName != "WulaSpecies")
|
||||
{
|
||||
if (PawnUtility.ShouldSendNotificationAbout(usedBy))
|
||||
{
|
||||
Messages.Message("WULA_MechSerumHealerNotWula".Translate(usedBy.Named("PAWN")).Resolve().StripTags() ?? "", usedBy, MessageTypeDefOf.NegativeEvent);
|
||||
}
|
||||
return; // 如果不是乌拉族,则不执行修复。
|
||||
}
|
||||
|
||||
int fixedCount = 0;
|
||||
// List<Hediff> fixedHediffs = new List<Hediff>(); // 此行不再需要,因为我们只计数
|
||||
|
||||
// 持续修复最严重的健康状况,直到没有更多可修复的状况。
|
||||
// HealthUtility.FixWorstHealthCondition 如果没有修复任何状况,会返回 null。
|
||||
while (HealthUtility.TryGetWorstHealthCondition(usedBy, out var hediffToFix, out var partToFix))
|
||||
{
|
||||
int initialHediffCount = usedBy.health.hediffSet.hediffs.Count;
|
||||
|
||||
TaggedString currentFixedMessage = HealthUtility.FixWorstHealthCondition(usedBy);
|
||||
|
||||
// 如果返回了消息,或者 Hediff 数量减少,或者特定的 Hediff 不再存在,则表示已修复。
|
||||
if (currentFixedMessage != null || usedBy.health.hediffSet.hediffs.Count < initialHediffCount || !usedBy.health.hediffSet.hediffs.Contains(hediffToFix))
|
||||
{
|
||||
fixedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果 FixWorstHealthCondition 返回 null 且未检测到变化,
|
||||
// 则表示此方法无法再修复更多状况。跳出循环。
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedCount > 0 && PawnUtility.ShouldSendNotificationAbout(usedBy))
|
||||
{
|
||||
Messages.Message("WULA_MechSerumHealerAllFixed".Translate(usedBy.Named("PAWN")), usedBy, MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
else if (fixedCount == 0 && PawnUtility.ShouldSendNotificationAbout(usedBy))
|
||||
{
|
||||
Messages.Message("WULA_MechSerumHealerNoConditionsFixed".Translate(usedBy.Named("PAWN")), usedBy, MessageTypeDefOf.NegativeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class Recipe_AdministerWulaMechRepairKit : Recipe_Surgery
|
||||
{
|
||||
public override void ApplyOnPawn(Pawn pawn, BodyPartRecord part, Pawn billDoer, List<Thing> ingredients, Bill bill)
|
||||
{
|
||||
// 调用基类的 ApplyOnPawn 方法,处理手术的通用逻辑
|
||||
base.ApplyOnPawn(pawn, part, billDoer, ingredients, bill);
|
||||
|
||||
// 查找作为成分的 WULA_MechRepairKit
|
||||
Thing mechRepairKit = null;
|
||||
foreach (Thing ingredient in ingredients)
|
||||
{
|
||||
if (ingredient.def.defName == "WULA_MechRepairKit")
|
||||
{
|
||||
mechRepairKit = ingredient;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mechRepairKit != null)
|
||||
{
|
||||
// 获取物品上的 CompUseEffect_FixAllHealthConditions 组件
|
||||
CompUseEffect_FixAllHealthConditions compUseEffect = mechRepairKit.TryGetComp<CompUseEffect_FixAllHealthConditions>();
|
||||
if (compUseEffect != null)
|
||||
{
|
||||
// 手动调用 DoEffect 方法,将病人作为 usedBy 传入
|
||||
compUseEffect.DoEffect(pawn);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"WULA_MechRepairKit is missing CompUseEffect_FixAllHealthConditions. This should not happen.");
|
||||
}
|
||||
|
||||
// 物品将由 CompProperties_UseEffectDestroySelf 销毁,因此此处无需手动销毁。
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Recipe_AdministerWulaMechRepairKit could not find WULA_MechRepairKit in ingredients for pawn {pawn.LabelShort}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user