This commit is contained in:
2025-08-19 16:17:16 +08:00
parent 4205273197
commit 0b2efcd559
9 changed files with 491 additions and 14 deletions

View File

@@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- 这是一个使用我们新系统的战争女皇示例 -->
<!-- 它被设置为自动生产,且生产不消耗资源 -->
<ThingDef ParentName="SuperHeavyMechanoid">
<defName>WULA_Mech_Warqueen</defName> <!-- 修改了defName以避免冲突 -->
<label>war queen (WULA)</label>
<description>An ultra-heavy mech with a built-in mech gestator. Fed with appropriate resources, the war queen can form small war urchin combat mechs within its massive carapace and deploy them into combat.\n\nEven more than other mechanoids, the war queen resembles a giant, living insect. All war mechs can be terrifying, but humans tend to find the war queen disturbing on a deeper level.</description>
<statBases>
<MoveSpeed>1.6</MoveSpeed>
<EnergyShieldRechargeRate>0.5</EnergyShieldRechargeRate>
<EnergyShieldEnergyMax>3</EnergyShieldEnergyMax>
<MeleeDoorDamageFactor>1.5</MeleeDoorDamageFactor>
<EMPResistance>0.7</EMPResistance>
</statBases>
<race>
<body>Mech_Warqueen</body>
<baseBodySize>4</baseBodySize>
<lifeStageAges>
<li>
<def>MechanoidFullyFormed</def>
<minAge>0</minAge>
<soundWounded>Pawn_Mech_Warqueen_Wounded</soundWounded>
<soundDeath>Pawn_Mech_Warqueen_Death</soundDeath>
<soundCall>Pawn_Mech_Warqueen_Call</soundCall>
</li>
<li>
<def>MechanoidFullyFormed</def>
<minAge>100</minAge>
<soundWounded>Pawn_Mech_Warqueen_Wounded</soundWounded>
<soundDeath>Pawn_Mech_Warqueen_Death</soundDeath>
<soundCall>Pawn_Mech_Warqueen_Call</soundCall>
</li>
</lifeStageAges>
<baseHealthScale>5.2</baseHealthScale>
<dutyBoss>Warqueen</dutyBoss>
<detritusLeavings>
<li>
<def>ChunkMechanoidSlag</def>
<texOverrideIndex>13</texOverrideIndex>
<spawnChance>0.75</spawnChance>
</li>
<li>
<def>ChunkMechanoidSlag</def>
<texOverrideIndex>3</texOverrideIndex>
<spawnChance>0.75</spawnChance>
</li>
<li>
<def>ChunkMechanoidSlag</def>
<texOverrideIndex>0</texOverrideIndex>
<spawnChance>0.75</spawnChance>
</li>
</detritusLeavings>
</race>
<tools>
<li>
<label>head</label>
<capacities>
<li>Blunt</li>
</capacities>
<power>4</power>
<cooldownTime>2</cooldownTime>
<linkedBodyPartsGroup>HeadAttackTool</linkedBodyPartsGroup>
<ensureLinkedBodyPartsGroupAlwaysUsable>true</ensureLinkedBodyPartsGroupAlwaysUsable>
<chanceFactor>0.2</chanceFactor>
</li>
</tools>
<comps>
<!-- 这里是我们修改的核心部分 -->
<li Class="WulaFallenEmpire.CompProperties_AutoMechCarrier">
<startsAsAutoSpawn>true</startsAsAutoSpawn>
<freeProduction>true</freeProduction>
<cooldownTicks>900</cooldownTicks> <!-- 15 seconds -->
<productionQueue>
<li>
<pawnKind>Mech_WarUrchin</pawnKind>
<count>2</count>
</li>
<li>
<pawnKind>Mech_Lancer</pawnKind>
<count>1</count>
</li>
</productionQueue>
<spawnEffecter>WarqueenWarUrchinsSpawned</spawnEffecter>
<spawnedMechEffecter>WarUrchinSpawned</spawnedMechEffecter>
</li>
<li Class="CompProperties_TurretGun">
<turretDef>WULA_Penetrating_BeamTurret</turretDef>
<angleOffset>-90</angleOffset>
<autoAttack>false</autoAttack>
<renderNodeProperties>
<li>
<nodeClass>PawnRenderNode_TurretGun</nodeClass>
<workerClass>PawnRenderNodeWorker_TurretGun</workerClass>
<overrideMeshSize>(1, 1)</overrideMeshSize>
<parentTagDef>Body</parentTagDef>
<baseLayer>20</baseLayer>
<pawnType>Any</pawnType>
<drawData>
<dataWest>
<rotationOffset>180</rotationOffset>
</dataWest>
</drawData>
</li>
</renderNodeProperties>
</li>
</comps>
<killedLeavingsPlayerHostile>
<PowerfocusChip>1</PowerfocusChip>
</killedLeavingsPlayerHostile>
</ThingDef>
<ThingDef ParentName="BaseWeaponTurret">
<defName>WULA_Penetrating_BeamTurret</defName>
<label>charge blaster turret</label>
<description>A small charge blaster designed for use on a defense turret.</description>
<tradeability>None</tradeability>
<destroyOnDrop>true</destroyOnDrop>
<graphicData>
<texPath>Things/Item/Equipment/WeaponRanged/ChargeBlasterTurret</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<statBases>
<Mass>2.6</Mass>
<AccuracyTouch>0.60</AccuracyTouch>
<AccuracyShort>0.80</AccuracyShort>
<AccuracyMedium>0.90</AccuracyMedium>
<AccuracyLong>0.85</AccuracyLong>
</statBases>
<verbs>
<li>
<verbClass>Verb_Shoot</verbClass>
<defaultProjectile>Bullet_WULA_RW_Penetrating_Beam_Ranged</defaultProjectile>
<range>44.9</range>
<ticksBetweenBurstShots>30</ticksBetweenBurstShots>
<soundCast>Shot_ChargeBlaster</soundCast>
<soundCastTail>GunTail_Heavy</soundCastTail>
<muzzleFlashScale>9</muzzleFlashScale>
<defaultCooldownTime>2.5</defaultCooldownTime>
<linkedBodyPartsGroup>BulbTurret</linkedBodyPartsGroup>
<ensureLinkedBodyPartsGroupAlwaysUsable>true</ensureLinkedBodyPartsGroupAlwaysUsable>
<ticksBetweenBurstShots>50</ticksBetweenBurstShots>
</li>
</verbs>
</ThingDef>
<PawnKindDef ParentName="HeavyMechanoidKind">
<defName>WULA_Mech_Warqueen</defName> <!-- 修改了defName以避免冲突 -->
<label>war queen (WULA)</label>
<labelPlural>war queens (WULA)</labelPlural>
<race>WULA_Mech_Warqueen</race>
<combatPower>600</combatPower>
<maxPerGroup>3</maxPerGroup>
<isBoss>true</isBoss>
<allowInMechClusters>false</allowInMechClusters>
<lifeStages>
<li>
<bodyGraphicData>
<texPath>Things/Pawn/Mechanoid/Warqueen</texPath>
<maskPath>Things/Pawn/Mechanoid/AllegianceOverlays/MechWarqueen</maskPath>
<shaderType>CutoutWithOverlay</shaderType>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>3</drawSize>
<shadowData>
<volume>(0.7, 0.8, 0.7)</volume>
</shadowData>
</bodyGraphicData>
</li>
<li>
<bodyGraphicData>
<texPath>Things/Pawn/Mechanoid/WarqueenAncient</texPath>
<maskPath>Things/Pawn/Mechanoid/AllegianceOverlays/MechWarqueen</maskPath>
<shaderType>CutoutWithOverlay</shaderType>
<graphicClass>Graphic_Multi</graphicClass>
<drawSize>3</drawSize>
<shadowData>
<volume>(0.7, 0.8, 0.7)</volume>
</shadowData>
</bodyGraphicData>
</li>
</lifeStages>
<controlGroupPortraitZoom>0.7</controlGroupPortraitZoom>
</PawnKindDef>
</Defs>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<!-- CompAutoMechCarrier -->
<MechCarrierDesc_Free>命令该单位生成 {0} 个 {1}。此操作不消耗任何资源。</MechCarrierDesc_Free>
<WULA_AutoSpawn_Label>自动生产</WULA_AutoSpawn_Label>
<WULA_AutoSpawn_Desc>切换是否自动生产单位。开启后,当资源和冷却允许时,会自动生产单位以维持队列上限。</WULA_AutoSpawn_Desc>
<WULA_AutoSpawn_On_Reason>自动生产已开启。再次点击以关闭。</WULA_AutoSpawn_On_Reason>
</LanguageData>

View File

@@ -0,0 +1,211 @@
using RimWorld;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Verse;
using Verse.AI.Group;
namespace WulaFallenEmpire
{
public class CompAutoMechCarrier : CompMechCarrier
{
private bool isAutoSpawning;
#region Reflected Fields
private static FieldInfo spawnedPawnsField;
private static FieldInfo cooldownTicksRemainingField;
private static FieldInfo innerContainerField;
private List<Pawn> SpawnedPawns
{
get
{
if (spawnedPawnsField == null)
spawnedPawnsField = typeof(CompMechCarrier).GetField("spawnedPawns", BindingFlags.NonPublic | BindingFlags.Instance);
return (List<Pawn>)spawnedPawnsField.GetValue(this);
}
}
private int CooldownTicksRemaining
{
get
{
if (cooldownTicksRemainingField == null)
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
return (int)cooldownTicksRemainingField.GetValue(this);
}
set
{
if (cooldownTicksRemainingField == null)
cooldownTicksRemainingField = typeof(CompMechCarrier).GetField("cooldownTicksRemaining", BindingFlags.NonPublic | BindingFlags.Instance);
cooldownTicksRemainingField.SetValue(this, value);
}
}
private ThingOwner InnerContainer
{
get
{
if (innerContainerField == null)
innerContainerField = typeof(CompMechCarrier).GetField("innerContainer", BindingFlags.NonPublic | BindingFlags.Instance);
return (ThingOwner)innerContainerField.GetValue(this);
}
}
#endregion
public CompProperties_AutoMechCarrier AutoProps => (CompProperties_AutoMechCarrier)props;
private int TotalPawnCapacity => AutoProps.productionQueue.Sum(e => e.count);
private int LiveSpawnedPawnsCount(PawnKindDef kind)
{
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
return SpawnedPawns.Count(p => p.kindDef == kind);
}
public override void PostSpawnSetup(bool respawningAfterLoad)
{
base.PostSpawnSetup(respawningAfterLoad);
if (!respawningAfterLoad)
{
isAutoSpawning = AutoProps.startsAsAutoSpawn;
}
}
public override void PostExposeData()
{
base.PostExposeData();
Scribe_Values.Look(ref isAutoSpawning, "isAutoSpawning", AutoProps.startsAsAutoSpawn);
}
private AcceptanceReport CanSpawnNow(PawnKindDef kind)
{
if (parent is Pawn pawn && (pawn.IsSelfShutdown() || !pawn.Awake() || pawn.Downed || pawn.Dead || !pawn.Spawned))
return false;
if (CooldownTicksRemaining > 0)
return "CooldownTime".Translate() + " " + CooldownTicksRemaining.ToStringSecondsFromTicks();
if (!AutoProps.freeProduction && InnerContainer.TotalStackCountOfDef(Props.fixedIngredient) < Props.costPerPawn)
return "MechCarrierNotEnoughResources".Translate();
return true;
}
private void TrySpawnPawn(PawnKindDef kind)
{
PawnGenerationRequest request = new PawnGenerationRequest(kind, parent.Faction, PawnGenerationContext.NonPlayer, -1, forceGenerateNewPawn: true);
Pawn pawn = PawnGenerator.GeneratePawn(request);
GenSpawn.Spawn(pawn, parent.Position, parent.Map);
SpawnedPawns.Add(pawn);
if (parent is Pawn p && p.GetLord() != null)
p.GetLord().AddPawn(pawn);
if (!AutoProps.freeProduction)
{
int costLeft = Props.costPerPawn;
List<Thing> things = new List<Thing>(InnerContainer);
for (int j = 0; j < things.Count; j++)
{
Thing thing = InnerContainer.Take(things[j], Mathf.Min(things[j].stackCount, costLeft));
costLeft -= thing.stackCount;
thing.Destroy();
if (costLeft <= 0) break;
}
}
CooldownTicksRemaining = Props.cooldownTicks;
if (Props.spawnedMechEffecter != null)
EffecterTrigger(Props.spawnedMechEffecter, Props.attachSpawnedMechEffecter, pawn);
if (Props.spawnEffecter != null)
EffecterTrigger(Props.spawnEffecter, Props.attachSpawnedEffecter, parent);
}
private void EffecterTrigger(EffecterDef effecterDef, bool attach, Thing target)
{
Effecter effecter = new Effecter(effecterDef);
effecter.Trigger(attach ? ((TargetInfo)target) : new TargetInfo(target.Position, target.Map), TargetInfo.Invalid);
effecter.Cleanup();
}
public override void CompTick()
{
base.CompTick();
if (isAutoSpawning && parent.IsHashIntervalTick(60)) // 每秒检查一次
{
if (CooldownTicksRemaining > 0) return;
foreach (var entry in AutoProps.productionQueue)
{
if (LiveSpawnedPawnsCount(entry.pawnKind) < entry.count)
{
if (CanSpawnNow(entry.pawnKind).Accepted)
{
TrySpawnPawn(entry.pawnKind);
break; // 每次只生产一个,然后等待下一次冷却
}
}
}
}
}
public override IEnumerable<Gizmo> CompGetGizmosExtra()
{
// 拦截并改造基类的Gizmo
foreach (var gizmo in base.CompGetGizmosExtra())
{
// 通过图标来稳定地识别目标按钮
if (gizmo is Command_ActionWithCooldown command && command.icon == ContentFinder<Texture2D>.Get("UI/Gizmos/ReleaseWarUrchins"))
{
// 我们只改造这个按钮,其他按钮原样返回
var modifiedCommand = new Command_ActionWithCooldown
{
// 保留冷却进度条的逻辑
cooldownPercentGetter = command.cooldownPercentGetter,
// 保留原版图标
icon = command.icon,
// 修改功能为切换自动生产
action = () => { isAutoSpawning = !isAutoSpawning; },
// 修改标签和描述
defaultLabel = "WULA_AutoSpawn_Label".Translate(),
defaultDesc = "WULA_AutoSpawn_Desc".Translate()
};
// 如果自动生产开启,则禁用按钮并显示红叉
if (isAutoSpawning)
{
modifiedCommand.Disable("WULA_AutoSpawn_On_Reason".Translate());
}
yield return modifiedCommand;
}
else
{
// 其他按钮(如开发者按钮)原样返回
yield return gizmo;
}
}
}
public override string CompInspectStringExtra()
{
SpawnedPawns.RemoveAll(p => p == null || p.Destroyed);
string text = "Pawns: " + SpawnedPawns.Count + " / " + TotalPawnCapacity;
foreach (var entry in AutoProps.productionQueue)
{
text += $"\n- {entry.pawnKind.LabelCap}: {LiveSpawnedPawnsCount(entry.pawnKind)} / {entry.count}";
}
if (CooldownTicksRemaining > 0)
{
text += "\n" + "CooldownTime".Translate() + ": " + CooldownTicksRemaining.ToStringSecondsFromTicks();
}
if (!AutoProps.freeProduction)
{
text += "\n" + base.CompInspectStringExtra();
}
return text;
}
}
}

View File

@@ -0,0 +1,54 @@
using RimWorld;
using Verse;
using System.Collections.Generic;
namespace WulaFallenEmpire
{
public class CompProperties_AutoMechCarrier : CompProperties_MechCarrier
{
// XML中定义初始是否为自动生产模式
public bool startsAsAutoSpawn = false;
// XML中定义生产是否消耗资源
public bool freeProduction = false;
// 定义生产队列
public List<PawnProductionEntry> productionQueue = new List<PawnProductionEntry>();
public CompProperties_AutoMechCarrier()
{
// 确保这个属性类指向我们新的功能实现类
compClass = typeof(CompAutoMechCarrier);
}
public override IEnumerable<string> ConfigErrors(ThingDef parentDef)
{
foreach (string error in base.ConfigErrors(parentDef))
{
yield return error;
}
if (productionQueue.NullOrEmpty())
{
yield return "CompProperties_AutoMechCarrier must have at least one entry in productionQueue.";
}
}
public override void ResolveReferences(ThingDef parentDef)
{
base.ResolveReferences(parentDef);
// Prevent division by zero if costPerPawn is not set, which the base game AI might try to access.
if (costPerPawn <= 0)
{
costPerPawn = 1;
}
// 如果spawnPawnKind为空因为我们用了新的队列系统
// 就从队列里取第一个作为“假”值以防止基类方法在生成Gizmo标签时出错。
if (spawnPawnKind == null && !productionQueue.NullOrEmpty())
{
spawnPawnKind = productionQueue[0].pawnKind;
}
}
}
}

View File

@@ -0,0 +1,17 @@
using Verse;
namespace WulaFallenEmpire
{
/// <summary>
/// A data class to hold information about a pawn to be produced in a queue.
/// Used in XML definitions.
/// </summary>
public class PawnProductionEntry
{
// The PawnKindDef of the unit to spawn.
public PawnKindDef pawnKind;
// The maximum number of this kind of unit to maintain.
public int count = 1;
}
}

View File

@@ -1,13 +0,0 @@
using Verse;
namespace WulaFallenEmpire
{
[StaticConstructorOnStartup]
public static class StartupLogger
{
static StartupLogger()
{
Log.Message("WulaFallenEmpire Mod DLL, version 1.0.2, has been loaded.");
}
}
}

View File

@@ -70,6 +70,9 @@
<ItemGroup>
<Compile Include="CompChargingBed.cs" />
<Compile Include="CompCleave.cs" />
<Compile Include="CompAutoMechCarrier.cs" />
<Compile Include="CompProperties_AutoMechCarrier.cs" />
<Compile Include="PawnProductionEntry.cs" />
<Compile Include="Building_Wula_DarkEnergy_Engine.cs" />
<Compile Include="CompApparelInterceptor.cs" />
<Compile Include="CompCustomUniqueWeapon.cs" />
@@ -154,7 +157,6 @@
<Compile Include="HediffComp_DamageResponse.cs" />
<Compile Include="JobDefOf_WULA.cs" />
<Compile Include="ThingDefOf_WULA.cs" />
<Compile Include="StartupLogger.cs" />
<Compile Include="WeaponSwitch.cs" />
<Compile Include="Verb\CompMultiStrike.cs" />
<Compile Include="Verb\Verb_MeleeAttack_MultiStrike.cs" />

View File

@@ -19,4 +19,13 @@ namespace WulaFallenEmpire
}
}
[StaticConstructorOnStartup]
public static class StartupLogger
{
static StartupLogger()
{
Log.Message("WulaFallenEmpire Mod DLL, version 1.0.2, has been loaded.");
}
}
}