17 Commits

Author SHA1 Message Date
ProjectKoi-Kalo\Kalo
268ea3c681 暂存 2025-09-22 21:50:05 +08:00
ProjectKoi-Kalo\Kalo
8699c4c63e 暂存 2025-09-22 21:09:04 +08:00
ProjectKoi-Kalo\Kalo
bb5e555acc 暂存 2025-09-22 20:45:29 +08:00
ProjectKoi-Kalo\Kalo
4c1cc31983 暂存 2025-09-22 20:25:58 +08:00
ProjectKoi-Kalo\Kalo
67c14918f8 暂存 2025-09-22 17:19:43 +08:00
ProjectKoi-Kalo\Kalo
7da2bcc223 暂存 2025-09-22 17:05:31 +08:00
ProjectKoi-Kalo\Kalo
db7e4393bc 暂存 2025-09-22 15:40:32 +08:00
ProjectKoi-Kalo\Kalo
0ac7be78ad 环保版酸液 2025-09-22 14:27:38 +08:00
ProjectKoi-Kalo\Kalo
2771ee610a 暂存喷吐版 2025-09-22 14:26:51 +08:00
ProjectKoi-Kalo\Kalo
23b1b6dd3b 暂存 2025-09-22 14:20:36 +08:00
ProjectKoi-Kalo\Kalo
0b555a7d3a 暂存 2025-09-22 14:07:57 +08:00
ProjectKoi-Kalo\Kalo
9e57d2e774 2025-09-22 13:48:50 +08:00
ProjectKoi-Kalo\Kalo
477f18f669 暂存 2025-09-22 13:47:41 +08:00
ProjectKoi-Kalo\Kalo
65860e9e43 Merge branch 'master' of https://git.ra3battle.cn/Kalospacer/ArachnaeSwarm 2025-09-22 12:37:07 +08:00
ProjectKoi-Kalo\Kalo
6e40cbb817 暂存 2025-09-22 12:35:38 +08:00
Tourswen
f56d572227 追猎种亚种,新资源活化钜菌 2025-09-22 12:02:46 +08:00
Tourswen
59857fda42 医药和坑道虫贴图 2025-09-22 00:05:30 +08:00
46 changed files with 2911 additions and 46 deletions

Binary file not shown.

View File

@@ -347,22 +347,21 @@
<cooldownTicksRange>6000</cooldownTicksRange>
<iconPath>ArachnaeSwarm/UI/Abilities/ARA_Toxic_Needle_Fire</iconPath>
<verbProperties>
<verbClass>Verb_AbilityShoot</verbClass>
<defaultProjectile>Bullet_ARA_RW_Basic_Fist_Needle_Gun</defaultProjectile>
<range>24.9</range>
<verbClass>Verb_CastAbility</verbClass>
<range>24</range>
<warmupTime>1</warmupTime>
<soundCast>Heatspikes_Shot</soundCast>
<soundCastTail>Heatspikes_Tail</soundCastTail>
<muzzleFlashScale>9</muzzleFlashScale>
<ticksBetweenBurstShots>6</ticksBetweenBurstShots>
<warmupTime>0</warmupTime>
<burstShotCount>12</burstShotCount>
<accuracyTouch>0.9</accuracyTouch>
<accuracyShort>0.8</accuracyShort>
<accuracyMedium>0.7</accuracyMedium>
<accuracyLong>0.6</accuracyLong>
<ai_IsWeapon>false</ai_IsWeapon>
<violent>false</violent>
<targetParams>
<canTargetPawns>True</canTargetPawns>
</targetParams>
</verbProperties>
<comps>
<li Class="ArachnaeSwarm.CompProperties_AbilityLaunchMultiProjectile">
<projectileDef>Bullet_ARA_RW_Basic_Fist_Needle_Gun</projectileDef>
<numProjectiles>12</numProjectiles>
</li>
<li Class="ArachnaeSwarm.CompProperties_AbilityNeedCost">
<needDef>Food</needDef>
<needCost>0.1</needCost>
@@ -776,4 +775,72 @@
<speed>40</speed>
</projectile>
</ThingDef>
<AbilityDef>
<defName>ARA_Fighter_Invisibility_jump</defName>
<label>追猎种跳跃</label>
<description>以强力的肌腱向目标地点跳跃,会暴露追猎种的身形。</description>
<iconPath>UI/Abilities/Longjump</iconPath>
<cooldownTicksRange>1000</cooldownTicksRange>
<charges>3</charges>
<cooldownPerCharge>true</cooldownPerCharge>
<hostile>false</hostile>
<casterMustBeCapableOfViolence>false</casterMustBeCapableOfViolence>
<verbProperties>
<verbClass>Verb_CastAbilityJump</verbClass>
<violent>false</violent>
<forceNormalTimeSpeed>false</forceNormalTimeSpeed>
<!-- <warmupTime>0.5</warmupTime> -->
<range>23</range>
<requireLineOfSight>false</requireLineOfSight>
<soundCast>Longjump_Jump</soundCast>
<soundLanding>Longjump_Land</soundLanding>
<targetParams>
<canTargetLocations>true</canTargetLocations>
<canTargetPawns>false</canTargetPawns>
<canTargetBuildings>false</canTargetBuildings>
</targetParams>
</verbProperties>
<jobDef>CastJump</jobDef>
<comps>
<li Class="ArachnaeSwarm.CompProperties_AbilityNeedCost">
<needDef>Food</needDef>
<needCost>0.1</needCost>
<failMessage>营养值不足,需要进食</failMessage>
</li>
</comps>
</AbilityDef>
<AbilityDef>
<defName>ARA_Fighter_Invisibility_Execution</defName>
<label>巨镰处决</label>
<description>追猎种挥动巨镰对敌人进行处决,造成一次高额伤害,处决完成后会暴露追猎种的身形。</description>
<iconPath>UI/Abilities/Longjump</iconPath>
<cooldownTicksRange>3800</cooldownTicksRange>
<cooldownPerCharge>true</cooldownPerCharge>
<hostile>false</hostile>
<casterMustBeCapableOfViolence>false</casterMustBeCapableOfViolence>
<stunTargetWhileCasting>true</stunTargetWhileCasting>
<warmupMote>Mote_HoraxSmallSpellWarmup</warmupMote>
<warmupEffecter>HoraxianAbilityCasting</warmupEffecter>
<warmupSound>AnomalyAbilityWarmup</warmupSound>
<jobDef>CastAbilityOnThingMelee</jobDef>
<verbProperties>
<verbClass>Verb_CastAbilityTouch</verbClass>
<drawAimPie>false</drawAimPie>
<range>-1</range>
<warmupTime>1</warmupTime>
<targetParams>
<canTargetSelf>false</canTargetSelf>
<canTargetMechs>false</canTargetMechs>
<canTargetBuildings>false</canTargetBuildings>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityGiveHediff">
<compClass>CompAbilityEffect_GiveHediff</compClass>
<hediffDef>ARA_Fighter_Execution_Damage</hediffDef>
<replaceExisting>true</replaceExisting>
</li>
</comps>
</AbilityDef>
</Defs>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- 女皇 -->
<!-- T0 -->
<HediffDef>
<defName>ARA_Queen_0_Stage</defName>
@@ -218,6 +219,7 @@
</comps>
</HediffDef>
<!-- 工艺 -->
<!-- T0 -->
<HediffDef>
<defName>ARA_WeaponSmith_0_Stage</defName>
@@ -356,6 +358,7 @@
<description>这只阿拉克涅蜜罐种正在产出虫蜜以滋养虫群。一只蜜罐种每天产出10份阿拉克涅虫蜜。</description>
<descriptionHyperlinks>
<ThingDef>ARA_InsectJelly</ThingDef>
<HediffDef>ARA_Myrmecocystus_Production_Medicine</HediffDef>
</descriptionHyperlinks>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.4, 0.8)</defaultLabelColor>
@@ -470,8 +473,8 @@
</AbilityDef>
<HediffDef>
<defName>ARA_Myrmecocystus_Production_Medicine</defName>
<label>医疗专精</label>
<description>这只阿拉克涅蜜罐种已经获得拔耀,不再生产虫蜜,而是生产巢穴所稀缺的药物、化学品,并可以操纵拟线种。</description>
<label>亚种-疗愈种</label>
<description>这只阿拉克涅蜜罐种已经获得拔耀,不再生产虫蜜,而是生产巢穴所稀缺的药物、化学品,并可以操纵拟线种寄生虫</description>
<descriptionHyperlinks>
<ThingDef>ARA_InsectJelly</ThingDef>
</descriptionHyperlinks>
@@ -497,4 +500,283 @@
</li>
</comps>
</HediffDef>
<!-- 战士种 -->
<HediffDef>
<defName>ARA_Fighter_Base</defName>
<label>虫巢战士</label>
<description>阿拉克涅的战士虫们可以将身体变成自己的最强大的武装,随着科技的解锁,它们将获得繁多的技能以供使用。</description>
<descriptionHyperlinks>
<HediffDef>ARA_Fighter_Invisibility</HediffDef>
</descriptionHyperlinks>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.4, 0.8)</defaultLabelColor>
<isBad>false</isBad>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<!-- <stages>
<li>
<becomeVisible>ture</becomeVisible>
</li>
<li>
<minSeverity>1.01</minSeverity>
<becomeVisible>false</becomeVisible>
</li>
</stages> -->
<comps>
<li Class="HediffCompProperties_GiveAbility">
<abilityDefs>
<li>ARA_AcidSprayBurst</li>
<li>ARA_Toxic_Needle_Fire</li>
<li>ARA_Fighter_Invisibility</li>
</abilityDefs>
</li>
<li Class="HediffCompProperties_RemoveIfOtherHediff">
<hediffs>
<li>ARA_Fighter_Invisibility</li>
</hediffs>
</li>
</comps>
</HediffDef>
<AbilityDef>
<defName>ARA_Fighter_Invisibility</defName>
<label>战士种转换——隐形杀手</label>
<description>使战士种发生内驱性进化,极大地损害其远程武器和特殊技能的使用能力,以换取近乎永久的隐形和强大的近战能力。\n\n该进化过程不可逆</description>
<iconPath>ArachnaeSwarm/UI/Abilities/ARA_Myrmecocystus_Production_Medicine</iconPath>
<cooldownTicksRange>1800</cooldownTicksRange>
<hostile>false</hostile>
<groupAbility>true</groupAbility>
<displayGizmoWhileUndrafted>true</displayGizmoWhileUndrafted>
<disableGizmoWhileUndrafted>false</disableGizmoWhileUndrafted>
<aiCanUse>true</aiCanUse>
<ai_IsOffensive>true</ai_IsOffensive>
<targetRequired>false</targetRequired>
<jobDef>CastAbilityOnThing</jobDef>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<range>1</range>
<warmupTime>12</warmupTime>
<soundCast>AcidSpray_Resolve</soundCast>
<violent>false</violent>
<targetable>false</targetable>
<targetParams>
<canTargetSelf>True</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityGiveHediff">
<compClass>CompAbilityEffect_GiveHediff</compClass>
<hediffDef>ARA_Fighter_Invisibility</hediffDef>
<onlyApplyToSelf>True</onlyApplyToSelf>
<replaceExisting>true</replaceExisting>
<severity>1</severity>
</li>
<li Class="ArachnaeSwarm.CompProperties_AbilityResearchPrereq">
<requiredResearch>ARA_Technology_1MED</requiredResearch>
<failMessage>需要科技 节点MED-1"制药" 以解锁进化</failMessage>
</li>
</comps>
</AbilityDef>
<HediffDef>
<defName>ARA_Fighter_Invisibility</defName>
<hediffClass>HediffWithComps</hediffClass>
<label>亚种-追猎种</label>
<description>经过进化的战士种获得了特殊的覆盖全身的虹细胞,这些含纳米晶体的细胞可以折射各处的光线以达到永久隐身状态,直到其发起攻击。此外,它们也获得了更强大的近距离作战能力,缺点则是复眼的退化使其难以进行射击。</description>
<isBad>false</isBad>
<stages>
<li>
<minSeverity>0.01</minSeverity>
<statFactors>
<RangedCooldownFactor>2</RangedCooldownFactor>
<ShootingAccuracyPawn>0.05</ShootingAccuracyPawn>
<AimingDelayFactor>5</AimingDelayFactor>
<StaggerDurationFactor>0</StaggerDurationFactor>
</statFactors>
</li>
</stages>
<comps>
<li Class="HediffCompProperties_Invisibility">
<visibleToPlayer>true</visibleToPlayer>
<fadeDurationTicks>60</fadeDurationTicks>
<recoverFromDisruptedTicks>90</recoverFromDisruptedTicks>
</li>
<li Class="HediffCompProperties_GiveAbility">
<abilityDefs>
<li>ARA_Fighter_Invisibility_Execution</li>
<li>ARA_Fighter_Invisibility_jump</li>
</abilityDefs>
</li>
<li Class="HediffCompProperties_DisappearsOnDeath" />
</comps>
</HediffDef>
<!-- 蜜罐种 -->
<HediffDef>
<defName>ARA_Smokepop_Base</defName>
<label>活体烟罐</label>
<description>迷雾种是阿拉克涅虫族中的大家伙,它们毕不仅拥有厚实的外壳,其灌满信息素浓烟的腹部可以喷出覆盖战场的气体,吸引那些较小的辅虫破土而出对敌方发起疯狂的攻击。</description>
<descriptionHyperlinks>
<HediffDef>ARA_Smokepop_Production_Bacterium</HediffDef>
</descriptionHyperlinks>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.4, 0.8)</defaultLabelColor>
<isBad>false</isBad>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<renderNodeProperties>
<li>
<nodeClass>PawnRenderNode_AttachmentHead</nodeClass>
<workerClass>PawnRenderNodeWorker_FlipWhenCrawling</workerClass>
<texPath>ArachnaeSwarm/Things/ARA_HiveNode/Addons/ArachnaeNode_Race_Addons_Smokepop_Armor</texPath>
<parentTagDef>Body</parentTagDef>
<useSkinShader>false</useSkinShader>
<useRottenColor>false</useRottenColor>
<rotDrawMode>Fresh, Rotting</rotDrawMode>
<drawSize>0.85</drawSize>
<drawData>
<dataNorth>
<offset>(0, 0, 0.2)</offset>
</dataNorth>
<dataEast>
<offset>(0, 0, 0.2)</offset>
</dataEast>
<dataSouth>
<offset>(0, 0, 0.2)</offset>
</dataSouth>
<dataWest>
<offset>(0, 0, 0.2)</offset>
</dataWest>
<defaultData>
<layer>800</layer>
</defaultData>
</drawData>
</li>
</renderNodeProperties>
<stages>
<li>
<minSeverity>0</minSeverity>
<statOffsets>
<ArmorRating_Blunt>0.75</ArmorRating_Blunt>
<ArmorRating_Sharp>0.8</ArmorRating_Sharp>
</statOffsets>
</li>
</stages>
<comps>
<li Class="HediffCompProperties_GiveAbility">
<abilityDefs>
<li>ARA_Ability_Smokepop</li>
<li>ARA_Smokepop_Production_Bacterium</li>
</abilityDefs>
</li>
<li Class="HediffCompProperties_RemoveIfOtherHediff">
<hediffs>
<li>ARA_Smokepop_Production_Bacterium</li>
</hediffs>
</li>
</comps>
</HediffDef>
<AbilityDef>
<defName>ARA_Smokepop_Production_Bacterium</defName>
<label>迷雾种转换——钜菌专精</label>
<description>使迷雾种发生内驱性进化,以牺牲喷射信息素和降低护甲为代价,使其可以定期产出阿拉克涅虫族的高级材料——活化钜菌。\n\n该进化过程不可逆</description>
<iconPath>ArachnaeSwarm/UI/Abilities/ARA_Myrmecocystus_Production_Medicine</iconPath>
<cooldownTicksRange>1800</cooldownTicksRange>
<hostile>false</hostile>
<groupAbility>true</groupAbility>
<displayGizmoWhileUndrafted>true</displayGizmoWhileUndrafted>
<disableGizmoWhileUndrafted>false</disableGizmoWhileUndrafted>
<aiCanUse>true</aiCanUse>
<ai_IsOffensive>true</ai_IsOffensive>
<targetRequired>false</targetRequired>
<jobDef>CastAbilityOnThing</jobDef>
<verbProperties>
<verbClass>Verb_CastAbility</verbClass>
<range>1</range>
<warmupTime>12</warmupTime>
<soundCast>AcidSpray_Resolve</soundCast>
<violent>false</violent>
<targetable>false</targetable>
<targetParams>
<canTargetSelf>True</canTargetSelf>
</targetParams>
</verbProperties>
<comps>
<li Class="CompProperties_AbilityGiveHediff">
<compClass>CompAbilityEffect_GiveHediff</compClass>
<hediffDef>ARA_Smokepop_Production_Bacterium</hediffDef>
<onlyApplyToSelf>True</onlyApplyToSelf>
<replaceExisting>true</replaceExisting>
<severity>1</severity>
</li>
<li Class="ArachnaeSwarm.CompProperties_AbilityResearchPrereq">
<requiredResearch>ARA_Technology_1MED</requiredResearch>
<failMessage>需要科技 节点MED-1"制药" 以解锁进化</failMessage>
</li>
</comps>
</AbilityDef>
<HediffDef>
<defName>ARA_Smokepop_Production_Bacterium</defName>
<label>亚种-育菌种</label>
<description>这只阿拉克涅迷雾种已经获得拔耀,前部甲壳脱落且无法再喷射信息素,转而换取了生产活化钜菌的能力。</description>
<descriptionHyperlinks>
<ThingDef>ARA_Activated_Bacterium</ThingDef>
</descriptionHyperlinks>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.4, 0.8)</defaultLabelColor>
<isBad>false</isBad>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<stages>
<li>
<minSeverity>0.01</minSeverity>
</li>
</stages>
<comps>
<li Class="ArachnaeSwarm.MoharHediffs.HediffCompProperties_Spawner">
<!--
==================================================
基础设置 (Basic Settings)
==================================================
-->
<!-- [DEBUG] 如果为true则为此组件启用详细的调试日志记录。 -->
<debug>true</debug>
<!-- 要生成的物品的ThingDef。 -->
<thingToSpawn>ARA_Activated_Bacterium</thingToSpawn>
<!-- 每次生成的基础物品数量。 -->
<spawnCount>3</spawnCount>
<!--
==================================================
生成周期 (Spawning Interval)
==================================================
-->
<!-- 下一次生成事件发生前的最少天数。 -->
<minDaysB4Next>1</minDaysB4Next>
<!-- 下一次生成事件发生前的最大天数。 -->
<maxDaysB4Next>1</maxDaysB4Next>
<randomGrace>0</randomGrace>
<!--
==================================================
与年龄相关的调整 (Age-Related Adjustments)
==================================================
-->
<!-- 如果为true生成数量将根据宿主的年龄进行调整。 -->
<ageWeightedQuantity>false</ageWeightedQuantity>
<!-- 如果为true且ageWeightedQuantity为true则随着宿主年龄增长生成数量变多。 -->
<olderBiggerQuantity>true</olderBiggerQuantity>
<!-- 如果为true且ageWeightedQuantity为true则随年龄增长的数量缩放将是指数性的而非线性的。 -->
<exponentialQuantity>true</exponentialQuantity>
<!-- 指数级数量缩放的最大乘数,以防止出现荒谬的数字。 -->
<exponentialRatioLimit>20</exponentialRatioLimit>
<!--
==================================================
生成条件 (Spawning Conditions)
==================================================
-->
<!-- 如果为true当宿主Pawn饥饿时生成将暂停。 -->
<hungerRelative>true</hungerRelative>
<!-- 如果为true当宿主Pawn受伤时生成将暂停。 -->
<healthRelative>false</healthRelative>
</li>
</comps>
</HediffDef>
</Defs>

View File

@@ -196,4 +196,88 @@
<li Class="HediffCompProperties_DisappearsOnDeath"/>
</comps>
</HediffDef>
<HediffDef>
<defName>ARA_ChainReload</defName>
<label>链式装填</label>
<description>使用的武器拥有链式装填能力, 装填速度将随每次射击后提升。</description>
<defaultLabelColor>(0.52, 1, 0.95)</defaultLabelColor>
<hediffClass>HediffWithComps</hediffClass>
<comps>
<li Class="HediffCompProperties_DisappearsOnDeath" />
<li Class="HediffCompProperties_Disappears">
<disappearsAfterTicks>1800</disappearsAfterTicks> <!-- 30 seconds -->
<showRemainingTime>true</showRemainingTime>
</li>
</comps>
<stages>
<li>
<label>等级1/5</label>
<becomeVisible>true</becomeVisible>
<statFactors>
<AimingDelayFactor>0.9</AimingDelayFactor>
<RangedCooldownFactor>0.9</RangedCooldownFactor>
</statFactors>
</li>
<li>
<label>等级2/5</label>
<minSeverity>4</minSeverity>
<statFactors>
<AimingDelayFactor>0.8</AimingDelayFactor>
<RangedCooldownFactor>0.8</RangedCooldownFactor>
</statFactors>
</li>
<li>
<label>等级3/5</label>
<minSeverity>8</minSeverity>
<statFactors>
<AimingDelayFactor>0.65</AimingDelayFactor>
<RangedCooldownFactor>0.65</RangedCooldownFactor>
</statFactors>
</li>
<li>
<label>等级4/5</label>
<minSeverity>12</minSeverity>
<statFactors>
<AimingDelayFactor>0.45</AimingDelayFactor>
<RangedCooldownFactor>0.45</RangedCooldownFactor>
</statFactors>
</li>
<li>
<label>等级5/5</label>
<minSeverity>16</minSeverity>
<statFactors>
<AimingDelayFactor>0.25</AimingDelayFactor>
<RangedCooldownFactor>0.25</RangedCooldownFactor>
</statFactors>
</li>
</stages>
</HediffDef>
<HediffDef>
<defName>ARA_Fighter_Execution_Damage</defName>
<label>处决</label>
<description></description>
<defaultLabelColor>(1, 1, 0.8)</defaultLabelColor>
<hediffClass>ArachnaeSwarm.HediffCurseFlame</hediffClass>
<comps>
<li Class="HediffCompProperties_Disappears">
<disappearsAfterTicks>2</disappearsAfterTicks>
</li>
<li Class="HediffCompProperties_DisappearsOnDeath"/>
</comps>
<modExtensions>
<li Class="ArachnaeSwarm.CurseFlameModExt">
<damageDefName>Cut</damageDefName>
<damageRange>300</damageRange>
<damageIntervalTicks>1</damageIntervalTicks>
</li>
</modExtensions>
<stages>
<li>
<label>minor</label>
<becomeVisible>false</becomeVisible>
</li>
</stages>
</HediffDef>
</Defs>

View File

@@ -196,6 +196,7 @@
<defName>ArachnaeNode_Race_Fighter</defName>
<label>阿拉克涅战士种</label>
<race>ArachnaeNode_Race_Fighter</race>
<collidesWithPawns>false</collidesWithPawns>
<defaultFactionType>PlayerColony</defaultFactionType>
<invNutrition>0</invNutrition>
<backstoryFiltersOverride>
@@ -208,8 +209,6 @@
</backstoryFiltersOverride>
<abilities>
<li>ARA_BaseRace_Acid_Launcher</li>
<li>ARA_AcidSprayBurst</li>
<li>ARA_Toxic_Needle_Fire</li>
</abilities>
<apparelTags>
</apparelTags>

View File

@@ -1091,6 +1091,17 @@
<tooltipOverride>阿拉克涅的督虫们生命如精密的发条般运转着。</tooltipOverride>
<showGizmoOnNonPlayerControlled>true</showGizmoOnNonPlayerControlled>
</li>
<li Class="ArachnaeSwarm.CompProperties_HediffGiver">
<hediffs>
<li>ARA_Fighter_Base</li>
</hediffs>
<addChance>1.0</addChance>
<allowDuplicates>false</allowDuplicates>
</li>
<li Class="ArachnaeSwarm.CompProperties_FighterInvisible">
<InvisibilityDef>ARA_Fighter_Invisibility</InvisibilityDef>
<stealthCooldownTicks>120</stealthCooldownTicks>
</li>
</comps>
</AlienRace.ThingDef_AlienRace>
<AlienRace.ThingDef_AlienRace ParentName="ARA_NodeBase">
@@ -1230,8 +1241,8 @@
<!-- <TradePriceImprovement>0.5</TradePriceImprovement> -->
<!-- 自带的甲壳可以防御外部攻击 -->
<ArmorRating_Blunt>1</ArmorRating_Blunt>
<ArmorRating_Sharp>1.2</ArmorRating_Sharp>
<ArmorRating_Blunt>0.25</ArmorRating_Blunt>
<ArmorRating_Sharp>0.4</ArmorRating_Sharp>
<ArmorRating_Heat>0.75</ArmorRating_Heat>
</statBases>

View File

@@ -6,8 +6,8 @@
<description>由阿拉克涅医药孵化茧所孵化的药物,效力等同于常规的医药,但是需要在冷藏条件下保存。</description>
<possessionCount>10</possessionCount>
<graphicData>
<texPath>Things/Item/Resource/Medicine/MedicineHerbal</texPath>
<graphicClass>Graphic_StackCount</graphicClass>
<texPath>ArachnaeSwarm/Item/ARA_Medicine</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>0.85</drawSize>
</graphicData>
<techLevel>Animal</techLevel>

View File

@@ -119,4 +119,33 @@
<allowedArchonexusCount>80</allowedArchonexusCount>
<possessionCount>50</possessionCount>
</ThingDef>
<ThingDef ParentName="ResourceBase">
<defName>ARA_Activated_Bacterium</defName>
<label>活化钜菌</label>
<description>一种被阿拉克涅虫族驯化和改造的极端微生物菌落。它具有超强的生物催化能力和腐蚀同化能力,是不少虫族高级装备的必需品。</description>
<graphicData>
<texPath>ArachnaeSwarm/Item/ARA_Carapace</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<genericMarketSellable>false</genericMarketSellable>
<soundInteract>Standard_Drop</soundInteract>
<soundDrop>Standard_Drop</soundDrop>
<useHitPoints>true</useHitPoints>
<healthAffectsPrice>false</healthAffectsPrice>
<stackLimit>30</stackLimit>
<statBases>
<MarketValue>1</MarketValue>
<MaxHitPoints>50</MaxHitPoints>
<Mass>0.01</Mass>
</statBases>
<thingCategories>
<li>ResourcesRaw</li>
</thingCategories>
<burnableByRecipe>false</burnableByRecipe>
<smeltable>false</smeltable>
<terrainAffordanceNeeded>Medium</terrainAffordanceNeeded>
<allowedArchonexusCount>80</allowedArchonexusCount>
<possessionCount>50</possessionCount>
</ThingDef>
</Defs>

View File

@@ -145,7 +145,7 @@
</li>
</tools>
<comps>
<li Class="WulaFallenEmpire.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_Damage_Toxid</li>
</forcedTraits>
@@ -229,7 +229,7 @@
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="WulaFallenEmpire.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_Damage_Toxid</li>
</forcedTraits>
@@ -303,7 +303,7 @@
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="WulaFallenEmpire.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_Damage_Acid</li>
</forcedTraits>
@@ -412,7 +412,7 @@
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="WulaFallenEmpire.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Huge_Weapon</li>
<li>ARA_Weapon_Damage_Acid</li>
@@ -523,7 +523,7 @@
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="WulaFallenEmpire.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_Damage_Spawn</li>
</forcedTraits>

View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<!-- spawn -->
<ThingDef ParentName="BaseHumanMakeableGun">
<defName>ARA_RW_Basic_SniperCannon_Gun</defName>
<label>武装器官"链式棘刺"</label>
<description>阿拉克涅虫群的生物武器</description>
<tickerType>Normal</tickerType>
<techLevel>Animal</techLevel>
<descriptionHyperlinks>
<ThingDef>ARA_Cocoon_Weapon_1Stage</ThingDef>
</descriptionHyperlinks>
<graphicData>
<texPath>ArachnaeSwarm/Weapon/ARA_RW_Basic_SniperCannon_Gun</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>1.2</drawSize>
</graphicData>
<soundInteract>SpitterSpawn</soundInteract>
<recipeMaker>
<recipeUsers Inherit="False" />
<researchPrerequisite>ARA_Technology_6SPV</researchPrerequisite>
<unfinishedThingDef>UnfinishedWeapon</unfinishedThingDef>
</recipeMaker>
<statBases>
<WorkToMake>1300</WorkToMake>
<!-- <MarketValue>370</MarketValue> -->
<Mass>3.5</Mass>
<AccuracyTouch>0.22</AccuracyTouch>
<AccuracyShort>0.33</AccuracyShort>
<AccuracyMedium>0.44</AccuracyMedium>
<AccuracyLong>0.95</AccuracyLong>
<RangedWeapon_Cooldown>2.2</RangedWeapon_Cooldown>
</statBases>
<verbs>
<li>
<verbClass>Verb_Shoot</verbClass>
<hasStandardCommand>true</hasStandardCommand>
<defaultProjectile>ARA_Bullet_SniperCannon</defaultProjectile>
<warmupTime>2.5</warmupTime>
<minRange>5.9</minRange>
<range>45.9</range>
<burstShotCount>1</burstShotCount>
<soundCast>Shot_TurretSniper</soundCast>
<soundCastTail>GunTail_Heavy</soundCastTail>
<muzzleFlashScale>18</muzzleFlashScale>
</li>
</verbs>
<costList Inherit="False">
<ARA_Carapace>50</ARA_Carapace>
</costList>
<weaponTags>
<li>ARA_Armed_Organ</li>
<li>ARA_Armed_Organ_Ranged</li>
<li>ARA_Armed_Organ_T1</li>
</weaponTags>
<thingSetMakerTags>
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="ArachnaeSwarm.CompProperties_GiveHediffOnShot">
<hediffDef>ARA_ChainReload</hediffDef>
<severityToAdd>0.5</severityToAdd>
</li>
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_ChainReload</li>
</forcedTraits>
<numTraitsRange>
<min>1</min>
<max>1</max>
</numTraitsRange>
</li>
</comps>
</ThingDef>
<ThingDef ParentName="BaseBullet">
<defName>ARA_Bullet_SniperCannon</defName>
<label>虫族棘刺</label>
<graphicData>
<texPath>Things/Projectile/Projectile_Fingerspike</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>1.4</drawSize>
</graphicData>
<projectile>
<damageDef>Bullet</damageDef>
<damageAmountBase>55</damageAmountBase>
<speed>120</speed>
</projectile>
</ThingDef>
<!---->
<ThingDef ParentName="BaseHumanMakeableGun">
<defName>ARA_RW_Basic_Acid_Spreay_Gun</defName>
<label>武装器官"酸液枪喷吐版"</label>
<description>阿拉克涅虫群督虫使用基础远程武装器官,可以通过肌肉的瞬间加压喷出一团包含阿拉克涅酸液的液体团。这种酸液团的飞行速度很慢,但是能在目标地点炸开,并灼烧所有粘上酸液的敌人。</description>
<tickerType>Normal</tickerType>
<techLevel>Animal</techLevel>
<descriptionHyperlinks>
<ThingDef>ARA_Cocoon_Weapon</ThingDef>
</descriptionHyperlinks>
<graphicData>
<texPath>ArachnaeSwarm/Weapon/ARA_RW_Basic_Acid_Bladder_Gun</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>1.2</drawSize>
</graphicData>
<soundInteract>SpitterSpawn</soundInteract>
<recipeMaker>
<recipeUsers Inherit="False" />
<researchPrerequisite>ARA_Technology_7VXI</researchPrerequisite>
<unfinishedThingDef>UnfinishedWeapon</unfinishedThingDef>
</recipeMaker>
<statBases>
<WorkToMake>1300</WorkToMake>
<!-- <MarketValue>370</MarketValue> -->
<Mass>3.5</Mass>
<AccuracyTouch>0.5</AccuracyTouch>
<AccuracyShort>0.6</AccuracyShort>
<AccuracyMedium>0.45</AccuracyMedium>
<AccuracyLong>0.3</AccuracyLong>
<RangedWeapon_Cooldown>2.5</RangedWeapon_Cooldown>
</statBases>
<verbs>
<li Class="ArachnaeSwarm.VerbProperties_ShootSprayMulti">
<verbClass>ArachnaeSwarm.Verb_ShootSprayMulti</verbClass>
<hasStandardCommand>true</hasStandardCommand>
<forceNormalTimeSpeed>false</forceNormalTimeSpeed>
<warmupTime>1.0</warmupTime>
<defaultProjectile>ARA_Proj_StrongSludgeSpray_Eco</defaultProjectile>
<isMortar>false</isMortar>
<requireLineOfSight>false</requireLineOfSight>
<minRange>3</minRange>
<range>28</range>
<burstShotCount>3</burstShotCount>
<ticksBetweenBurstShots>12</ticksBetweenBurstShots>
<soundCast>SpitterSpit</soundCast>
<targetParams>
<canTargetLocations>true</canTargetLocations>
</targetParams>
<numCellsToHit>5</numCellsToHit>
</li>
</verbs>
<costList Inherit="False">
<ARA_Carapace>50</ARA_Carapace>
</costList>
<weaponTags>
<li>ARA_Armed_Organ</li>
<li>ARA_Armed_Organ_Ranged</li>
<li>ARA_Armed_Organ_T1</li>
</weaponTags>
<thingSetMakerTags>
<li>RewardStandardQualitySuper</li>
</thingSetMakerTags>
<comps>
<li Class="ArachnaeSwarm.CompProperties_CustomUniqueWeapon" MayRequire="Ludeon.RimWorld.Odyssey">
<forcedTraits>
<li>ARA_Weapon_Damage_Acid</li>
</forcedTraits>
<numTraitsRange>
<min>1</min>
<max>1</max>
</numTraitsRange>
</li>
</comps>
</ThingDef>
<ThingDef>
<defName>ARA_Proj_StrongSludgeSpray_Eco</defName>
<label>阿拉克涅虫族酸液</label>
<thingClass>Projectile_Liquid</thingClass>
<category>Projectile</category>
<tickerType>Normal</tickerType>
<altitudeLayer>Projectile</altitudeLayer>
<useHitPoints>False</useHitPoints>
<neverMultiSelect>True</neverMultiSelect>
<graphicData>
<texPath>Things/Projectile/SludgeSpray</texPath>
<graphicClass>Graphic_Random</graphicClass>
<shaderType>MoteGlow</shaderType>
<drawSize>0.85</drawSize>
</graphicData>
<projectile>
<damageDef>ARA_AcidBurn</damageDef>
<speed>25</speed>
<damageAmountBase>2</damageAmountBase>
<arcHeightFactor>0.4</arcHeightFactor>
<armorPenetrationBase>0.1</armorPenetrationBase>
<stoppingPower>1</stoppingPower>
<shadowSize>0</shadowSize>
<terrainChance>0.75</terrainChance>
<filth>Filth_SpentAcid</filth>
<filthCount>1</filthCount>
<filthChance>0.25</filthChance>
</projectile>
</ThingDef>
</Defs>

View File

@@ -580,14 +580,16 @@
<commandLabelKey>CommandDesignateOpenCloseVentLabel</commandLabelKey>
<commandDescKey>CommandDesignateOpenCloseVentDesc</commandDescKey>
</li>
<!-- 提供温度控制UI和逻辑 -->
<li Class="ArachnaeSwarm.CompProperties_TempControl_Fixed">
<!-- 这是设备的热交换功率。数值越大,制冷/制热速度越快。-->
<energyPerSecond>2</energyPerSecond>
</li>
<li Class="CompProperties_Breakdownable"/>
<li Class="ArachnaeSwarm.CompProperties_DelayedTerrainSpawn">
<terrainToSpawn>ARA_InsectCreep</terrainToSpawn>
<spawnRadius>3</spawnRadius>
</li>
</comps>
</ThingDef>
</Defs>

View File

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>CatastropheMissileSilo</defName>
<label>天灾导弹发射井</label>
<description>一个多功能导弹发射平台。它装备的武器系统既可以作为自动炮塔进行本地防御,也可以在操作员的指引下,将“天灾”级巡航导弹发射到全球任何一个角落。</description>
<thingClass>ArachnaeSwarm.Building_CatastropheMissileSilo</thingClass>
<drawerType>MapMeshAndRealTime</drawerType>
<graphicData>
<texPath>Things/Building/Security/TurretHeavy_Base</texPath>
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(3, 3)</drawSize>
<drawOffset>(0,0,-0.1)</drawOffset>
<damageData>
<rect>(0.2,0.2,0.6,0.6)</rect>
</damageData>
<shadowData>
<volume>(1.5,0.35,1.4)</volume>
<offset>(0,0,-0.05)</offset>
</shadowData>
</graphicData>
<size>(2,2)</size>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<pathCost>50</pathCost>
<fillPercent>0.5</fillPercent>
<stealable>false</stealable>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>12000</WorkToBuild>
<Mass>800</Mass>
<Beauty>-20</Beauty>
</statBases>
<tickerType>Normal</tickerType>
<comps>
<li Class="CompProperties_Refuelable">
<fuelLabel>导弹</fuelLabel>
<fuelGizmoLabel>导弹</fuelGizmoLabel>
<outOfFuelMessage>缺少导弹</outOfFuelMessage>
<fuelFilter>
<thingDefs>
<li>ComponentSpacer</li>
</thingDefs>
</fuelFilter>
<fuelCapacity>10</fuelCapacity>
<initialFuelPercent>0</initialFuelPercent>
<autoRefuelPercent>1</autoRefuelPercent>
<showFuelGizmo>true</showFuelGizmo>
<consumeFuelOnlyWhenUsed>true</consumeFuelOnlyWhenUsed>
</li>
<li Class="CompProperties_Forbiddable"/>
<li Class="CompProperties_Breakdownable"/>
<li Class="ArachnaeSwarm.CompProperties_ForceTargetable" />
</comps>
<building>
<turretGunDef>CatastropheMissile_Weapon</turretGunDef>
<turretBurstCooldownTime>5.0</turretBurstCooldownTime>
<buildingTags>
<li>Artillery</li>
</buildingTags>
</building>
<designationCategory>ARA_Buildings</designationCategory>
<constructionSkillPrerequisite>8</constructionSkillPrerequisite>
<researchPrerequisites>
<li>ShipbuildingBasics</li>
</researchPrerequisites>
</ThingDef>
<ThingDef ParentName="BaseBullet">
<defName>Projectile_CatastropheMissile</defName>
<label>“天灾”巡航导弹</label>
<thingClass>ArachnaeSwarm.Projectile_CruiseMissile</thingClass>
<graphicData>
<graphicClass>Graphic_Single_AgeSecs</graphicClass>
<texPath>Things/Projectile/FleshmassSpitterProjectileSheet</texPath>
<drawSize>(3,3)</drawSize>
<shaderType>MoteGlow</shaderType>
</graphicData>
<projectile>
<useGraphicClass>True</useGraphicClass>
<shadowSize>1</shadowSize>
<damageDef>ARA_AcidBurn</damageDef>
<damageAmountBase>150</damageAmountBase>
<speed>80</speed>
<flyOverhead>true</flyOverhead>
<filth>Filth_SpentAcid</filth>
<filthCount>4</filthCount>
<explosionEffect>ARA_Shell_AcidSpitImpact</explosionEffect>
<explosionEffectLifetimeTicks>60</explosionEffectLifetimeTicks>
<doExplosionVFX>false</doExplosionVFX>
<explosionRadius>10.9</explosionRadius>
<soundExplode>MortarBomb_Explode</soundExplode>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.CruiseMissileProperties">
<customDamageDef>ARA_AcidBurn</customDamageDef>
<customDamageAmount>150</customDamageAmount>
<customExplosionRadius>10.9</customExplosionRadius>
<customSoundExplode>MortarBomb_Explode</customSoundExplode>
<useSubExplosions>true</useSubExplosions>
<subExplosionCount>8</subExplosionCount>
<subExplosionRadius>2.9</subExplosionRadius>
<subExplosionDamage>50</subExplosionDamage>
<subExplosionSpread>15</subExplosionSpread>
<subDamageDef>ARA_AcidBurn</subDamageDef>
<subSoundExplode>MortarBomb_Explode</subSoundExplode>
<bezierArcHeightFactor>0.01</bezierArcHeightFactor>
<bezierMinArcHeight>1</bezierMinArcHeight>
<bezierMaxArcHeight>5</bezierMaxArcHeight>
<bezierHorizontalOffsetFactor>0.05</bezierHorizontalOffsetFactor>
<bezierSideOffsetFactor>0.05</bezierSideOffsetFactor>
<bezierRandomOffsetScale>1.5</bezierRandomOffsetScale>
</li>
</modExtensions>
<comps>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitStream</effecterDef>
</li>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitLaunched</effecterDef>
</li>
</comps>
</ThingDef>
<EffecterDef>
<defName>ARA_Shell_AcidSpitImpact</defName>
<children>
<li>
<subEffecterClass>SubEffecter_SprayerChance</subEffecterClass>
<fleckDef>Fleck_AcidSpitImpact</fleckDef>
<burstCount>1</burstCount>
<scale>5</scale>
<chancePerTick>1</chancePerTick>
<chancePeriodTicks>2</chancePeriodTicks>
<lifespanMaxTicks>8</lifespanMaxTicks>
<spawnLocType>OnSource</spawnLocType>
</li>
<li>
<subEffecterClass>SubEffecter_SprayerTriggered</subEffecterClass>
<fleckDef>Fleck_AcidSpitLaunchedMist</fleckDef>
<scale>20</scale>
<burstCount>3~6</burstCount>
<spawnLocType>OnSource</spawnLocType>
<absoluteAngle>false</absoluteAngle>
<angle>0~100</angle>
<positionRadius>1</positionRadius>
<speed>-1~1</speed>
<rotation>0</rotation>
</li>
<li>
<subEffecterClass>SubEffecter_SprayerTriggered</subEffecterClass>
<fleckDef>Fleck_AcidSpitLaunchedGlobFast</fleckDef>
<burstCount>6~10</burstCount>
<spawnLocType>OnSource</spawnLocType>
<absoluteAngle>false</absoluteAngle>
<positionRadius>.7</positionRadius>
<positionRadiusMin>.7</positionRadiusMin>
<fleckUsesAngleForVelocity>true</fleckUsesAngleForVelocity>
<rotateTowardsTargetCenter>true</rotateTowardsTargetCenter>
<angle>0~100</angle>
<scale>2.5</scale>
<speed>20~45</speed>
<rotation>0~360</rotation>
</li>
</children>
</EffecterDef>
<ThingDef ParentName="BaseBullet">
<defName>Projectile_CatastropheMissile_Fake</defName>
<label>“天灾”巡航导弹</label>
<thingClass>ArachnaeSwarm.Projectile_CruiseMissile</thingClass>
<graphicData>
<graphicClass>Graphic_Single_AgeSecs</graphicClass>
<texPath>Things/Projectile/FleshmassSpitterProjectileSheet</texPath>
<drawSize>(3,3)</drawSize>
<shaderType>MoteGlow</shaderType>
</graphicData>
<projectile>
<useGraphicClass>True</useGraphicClass>
<shadowSize>1</shadowSize>
<damageDef>ARA_AcidBurn</damageDef>
<damageAmountBase>0</damageAmountBase>
<speed>80</speed>
<flyOverhead>true</flyOverhead>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.CruiseMissileProperties">
<isDummy>true</isDummy>
<useSubExplosions>false</useSubExplosions>
<bezierArcHeightFactor>0.01</bezierArcHeightFactor>
<bezierMinArcHeight>1</bezierMinArcHeight>
<bezierMaxArcHeight>5</bezierMaxArcHeight>
<bezierHorizontalOffsetFactor>0.05</bezierHorizontalOffsetFactor>
<bezierSideOffsetFactor>0.05</bezierSideOffsetFactor>
<bezierRandomOffsetScale>1.5</bezierRandomOffsetScale>
</li>
</modExtensions>
<comps>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitStream</effecterDef>
</li>
<li Class="CompProperties_ProjectileEffecter">
<effecterDef>Shell_AcidSpitLaunched</effecterDef>
</li>
</comps>
</ThingDef>
<ThingDef ParentName="BaseWeapon">
<defName>CatastropheMissile_Weapon</defName>
<label>天灾导弹武器系统</label>
<description>天灾导弹的发射系统。</description>
<techLevel>Spacer</techLevel>
<graphicData>
<texPath>Things/Building/Security/TurretMortar_Top</texPath>
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<statBases>
<RangedWeapon_Cooldown>5.0</RangedWeapon_Cooldown>
<Mass>50</Mass>
</statBases>
<verbs>
<li>
<verbClass>Verb_Shoot</verbClass>
<hasStandardCommand>true</hasStandardCommand>
<defaultProjectile>Projectile_CatastropheMissile</defaultProjectile>
<consumeFuelPerShot>1</consumeFuelPerShot>
<warmupTime>3.0</warmupTime>
<forcedMissRadius>1</forcedMissRadius>
<isMortar>true</isMortar>
<requireLineOfSight>false</requireLineOfSight>
<minRange>10.9</minRange>
<burstShotCount>1</burstShotCount>
<range>500</range>
<soundCast>Shot_Autocannon</soundCast>
<muzzleFlashScale>16</muzzleFlashScale>
<targetParams>
<canTargetLocations>true</canTargetLocations>
</targetParams>
</li>
</verbs>
</ThingDef>
</Defs>

View File

@@ -13,7 +13,7 @@
<shaderType>CutoutComplex</shaderType>
<drawSize>(4,4)</drawSize>
<shadowData>
<volume>(1.6, 0.5, 1.6)</volume>
<volume>(2.6, 2.5, 1.6)</volume>
<offset>(0,0,-0.1)</offset>
</shadowData>
</graphicData>
@@ -27,7 +27,8 @@
<Flammability>0.5</Flammability>
<Mass>100</Mass>
</statBases>
<size>(2,2)</size>
<uiIconScale>0.85</uiIconScale>
<size>(4,4)</size>
<costList>
<ARA_Carapace>50</ARA_Carapace>
</costList>
@@ -59,6 +60,10 @@
<li>ARA_NutrientNetworkTower</li>
</linkableFacilities>
</li>
<li Class="ArachnaeSwarm.CompProperties_DelayedTerrainSpawn">
<terrainToSpawn>ARA_InsectCreep</terrainToSpawn>
<spawnRadius>5</spawnRadius>
</li>
</comps>
<designationCategory>ARA_Buildings</designationCategory>
<terrainAffordanceNeeded>ARA_Creep</terrainAffordanceNeeded>
@@ -82,7 +87,7 @@
<shaderType>CutoutComplex</shaderType>
<drawSize>(4,4)</drawSize>
<shadowData>
<volume>(1.6, 0.5, 1.6)</volume>
<volume>(2.6, 2.5, 1.6)</volume>
<offset>(0,0,-0.1)</offset>
</shadowData>
</graphicData>
@@ -101,6 +106,10 @@
<colorPickerEnabled>true</colorPickerEnabled>
</li>
<li Class="CompProperties_Flickable"/>
<li Class="ArachnaeSwarm.CompProperties_DelayedTerrainSpawn">
<terrainToSpawn>ARA_InsectCreep</terrainToSpawn>
<spawnRadius>12</spawnRadius>
</li>
</comps>
<tradeability>None</tradeability>
<portal>

View File

@@ -44,4 +44,15 @@
<statFactors>
</statFactors>
</WeaponTraitDef>
<WeaponTraitDef>
<defName>ARA_Weapon_ChainReload</defName>
<label>生物链式重装填</label>
<description>这种武装器官会随着每次攻击释放激素加快代谢速度,从而体现为射击速度和装填速度加快。</description>
<commonality>1</commonality>
<weaponCategory>ARA_Weapon_Damage_Category</weaponCategory>
<statOffsets>
</statOffsets>
<statFactors>
</statFactors>
</WeaponTraitDef>
</Defs>

View File

@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<WorldObjectDef>
<defName>ARA_TravelingWormhole</defName>
<label>掘进中的虫洞</label>
<worldObjectClass>ArachnaeSwarm.TravelingWormhole</worldObjectClass>
<texture>World/WorldObjects/Caravan</texture>
<texture>ArachnaeSwarm/World/WorldObjects/Expanding/ARA_TravelingWormhole</texture>
<useDynamicDrawer>true</useDynamicDrawer>
<expandingIcon>true</expandingIcon>
<expandingIconTexture>World/WorldObjects/Expanding/Caravan</expandingIconTexture>
<expandingIconTexture>ArachnaeSwarm/World/WorldObjects/ARA_TravelingWormhole</expandingIconTexture>
<expandingIconPriority>100</expandingIconPriority>
<expandMore>true</expandMore>
<modExtensions>
@@ -17,5 +16,4 @@
</li>
</modExtensions>
</WorldObjectDef>
</Defs>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<WorldObjectDef>
<defName>CatastropheMissile_Flying</defName>
<label>巡航导弹</label>
<worldObjectClass>ArachnaeSwarm.WorldObject_CatastropheMissile</worldObjectClass>
<texture>World/WorldObjects/Caravan</texture> <!-- Placeholder texture -->
<expandingIcon>true</expandingIcon>
<useDynamicDrawer>true</useDynamicDrawer>
</WorldObjectDef>
</Defs>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<CommandFireGlobal>发射远程打击</CommandFireGlobal>
<CommandFireGlobalDesc>选择一个世界地图上的目标进行远程打击。</CommandFireGlobalDesc>
<CommandFireLocal>发射本地打击</CommandFireLocal>
<CommandFireLocalDesc>手动命令炮塔攻击一个本地地图上的目标。</CommandFireLocalDesc>
<RemoteTargetSet>远程目标已设定:{0}</RemoteTargetSet>
<MessageTargetMustBeMap>远程打击的目标必须是一个已探索的地点。</MessageTargetMustBeMap>
<NoMissileToLaunch>没有可用的导弹。</NoMissileToLaunch>
<CommandClearAllTargets>取消所有目标</CommandClearAllTargets>
<CommandClearAllTargetsDesc>取消当前设定的所有本地和远程目标。</CommandClearAllTargetsDesc>
</LanguageData>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<LanguageData>
<!-- Commands & Messages -->
<CommandDeployWormholePortalB_Pilot>部署虫洞传送门</CommandDeployWormholePortalB_Pilot>
<CommandDeployWormholePortalB_PilotDesc>选择一名驾驶员来启动一个B端传送门。</CommandDeployWormholePortalB_PilotDesc>
<NoPilotAvailable>没有可用的驾驶员</NoPilotAvailable>
</LanguageData>

View File

@@ -0,0 +1,468 @@
# 天灾导弹防御塔 - C# 实现计划
本文档包含构建“天灾导弹防御塔”所需的全部C#类的源代码。这些代码基于 `Rimatomics` 的跨地图打击框架,并与我们自己的 `Projectile_CruiseMissile` 弹头相结合。
---
## 1. 建筑类: `Building_CatastropheMissileSilo.cs`
**路径**: `Source/ArachnaeSwarm/Buildings/Building_CatastropheMissileSilo.cs`
**功能**: 这是导弹发射井的建筑本体,负责处理玩家的瞄准指令。它几乎是 `Building_Railgun` 的翻版,但进行了一些重命名和命名空间调整。
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_CatastropheMissileSilo : Building
{
public CompPowerTrader powerComp;
public CompRefuelable refuelableComp;
public Verb_LaunchCatastropheMissile verb;
private GlobalTargetInfo longTargetInt;
private List<Building_CatastropheMissileSilo> selectedSilos;
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("UI/Commands/Attack", true);
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
this.powerComp = base.GetComp<CompPowerTrader>();
this.refuelableComp = base.GetComp<CompRefuelable>();
// This is a placeholder, verb will be initialized from the weapon ThingDef
this.verb = (Verb_LaunchCatastropheMissile)Activator.CreateInstance(typeof(Verb_LaunchCatastropheMissile));
this.verb.caster = this;
}
public override void ExposeData()
{
base.ExposeData();
Scribe_TargetInfo.Look(ref this.longTargetInt, "longTargetInt");
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo c in base.GetGizmos())
{
yield return c;
}
Command_Action launch = new Command_Action
{
defaultLabel = "CommandFireMission".Translate(),
defaultDesc = "CommandFireMissionDesc".Translate(),
icon = FireMissionTex,
action = new Action(this.StartChoosingDestination)
};
if (!CanFire())
{
launch.Disable("CannotFire".Translate() + ": " + GetDisabledReason());
}
yield return launch;
}
private bool CanFire()
{
return powerComp.PowerOn && refuelableComp.HasFuel;
}
private string GetDisabledReason()
{
if (!powerComp.PowerOn) return "NoPower".Translate().CapitalizeFirst();
if (!refuelableComp.HasFuel) return "NoFuel".Translate().CapitalizeFirst();
return "Unknown";
}
private void StartChoosingDestination()
{
this.selectedSilos = Find.Selector.SelectedObjects.OfType<Building_CatastropheMissileSilo>().ToList();
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
true, // Can target self
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, this.MaxWorldRange),
(GlobalTargetInfo t) => "Select target",
null, null, true);
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
int distance = Find.WorldGrid.TraversalDistanceBetween(this.Map.Tile, target.Tile, true, int.MaxValue);
if (distance > this.MaxWorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return false;
}
MapParent mapParent = target.WorldObject as MapParent;
if (mapParent != null && mapParent.HasMap)
{
Map targetMap = mapParent.Map;
var originalMap = base.Map;
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap) Current.Game.CurrentMap = originalMap;
};
Current.Game.CurrentMap = targetMap;
Find.Targeter.BeginTargeting(
new TargetingParameters { canTargetLocations = true },
(LocalTargetInfo localTarget) =>
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID);
}
},
null, onFinished, FireMissionTex, true);
return true;
}
else // For non-map targets (caravans, sites)
{
foreach (var silo in this.selectedSilos)
{
silo.FireMission(target.Tile, new LocalTargetInfo(target.Cell), -1);
}
return true;
}
}
public void FireMission(int tile, LocalTargetInfo targ, int mapId)
{
if (!targ.IsValid)
{
this.longTargetInt = GlobalTargetInfo.Invalid;
return;
}
Map targetMap = (mapId != -1) ? Find.Maps.FirstOrDefault(m => m.uniqueID == mapId) : null;
this.longTargetInt = new GlobalTargetInfo(targ.Cell, targetMap);
this.verb.verbProps.defaultProjectile.GetModExtension<CruiseMissileProperties>(); // Ensure verb has properties.
this.verb.TryStartCastOn(this.longTargetInt);
}
public int MaxWorldRange => 99999; // Effectively global range
public GlobalTargetInfo LongTarget => longTargetInt;
}
}
```
---
## 2. 动作类: `Verb_LaunchCatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/Verbs/Verb_LaunchCatastropheMissile.cs`
**功能**: 定义发射动作。它会创建 `WorldObject_CatastropheMissile` 并将其发射到世界地图。
```csharp
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace ArachnaeSwarm
{
public class Verb_LaunchCatastropheMissile : Verb_Shoot
{
public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ)
{
return true; // Always true for world-map targeting
}
protected override bool TryCastShot()
{
Building_CatastropheMissileSilo silo = this.caster as Building_CatastropheMissileSilo;
if (silo == null || !silo.LongTarget.IsValid) return false;
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = silo.Map.Tile;
missile.destinationTile = silo.LongTarget.Tile;
missile.destinationCell = silo.LongTarget.Cell;
missile.Projectile = this.verbProps.defaultProjectile;
Find.WorldObjects.Add(missile);
// Consume fuel
silo.refuelableComp.ConsumeFuel(silo.refuelableComp.Props.fuelConsumptionRate);
// Visual/Sound effects at launch site
MoteMaker.MakeStaticMote(silo.TrueCenter(), silo.Map, ThingDefOf.Mote_ExplosionFlash, 10f);
SoundDefOf.RocketLaunch.PlayOneShot(new TargetInfo(silo.Position, silo.Map));
return true;
}
}
}
```
---
## 3. 飞行物类: `WorldObject_CatastropheMissile.cs`
**路径**: `Source/ArachnaeSwarm/World/WorldObject_CatastropheMissile.cs`
**功能**: 模拟导弹在世界地图上的飞行,并在抵达时生成 `Projectile_CruiseMissile`
```csharp
using RimWorld.Planet;
using UnityEngine;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class WorldObject_CatastropheMissile : WorldObject
{
public int destinationTile = -1;
public IntVec3 destinationCell = IntVec3.Invalid;
public ThingDef Projectile;
private int initialTile = -1;
private float traveledPct;
private const float TravelSpeed = 0.0002f; // Faster than sabot
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref destinationTile, "destinationTile", 0);
Scribe_Values.Look(ref destinationCell, "destinationCell");
Scribe_Defs.Look(ref Projectile, "Projectile");
Scribe_Values.Look(ref initialTile, "initialTile", 0);
Scribe_Values.Look(ref traveledPct, "traveledPct", 0f);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = this.Tile;
}
private Vector3 StartPos => Find.WorldGrid.GetTileCenter(this.initialTile);
private Vector3 EndPos => Find.WorldGrid.GetTileCenter(this.destinationTile);
public override Vector3 DrawPos => Vector3.Slerp(StartPos, EndPos, traveledPct);
public override void Tick()
{
base.Tick();
traveledPct += TravelSpeed / GenMath.SphericalDistance(StartPos.normalized, EndPos.normalized);
if (traveledPct >= 1f)
{
Arrived();
}
}
private void Arrived()
{
Map targetMap = Current.Game.FindMap(this.destinationTile);
if (targetMap != null)
{
// Target is a loaded map, spawn the projectile to hit it
IntVec3 entryCell = Find.WorldGrid.GetRotatedPos(new IntVec2(0, 1), targetMap.info.parent.Rotation).ToIntVec3() * (targetMap.Size.x / 2);
entryCell.y = 0;
entryCell.z += targetMap.Size.z / 2;
Projectile_CruiseMissile missile = (Projectile_CruiseMissile)GenSpawn.Spawn(this.Projectile, entryCell, targetMap, WipeMode.Vanish);
missile.Launch(this, this.destinationCell, this.destinationCell, ProjectileHitFlags.IntendedTarget);
}
else
{
// Target is not a loaded map (e.g., caravan, site), do direct damage
GenExplosion.DoExplosion(
this.destinationCell,
null, // No map
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customExplosionRadius ?? 5f,
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageDef ?? DamageDefOf.Bomb,
null, // Launcher
this.Projectile.GetModExtension<CruiseMissileProperties>()?.customDamageAmount ?? 50
);
}
Find.WorldObjects.Remove(this);
}
}
}
---
## 4. XML
XML定义
### 4.1 : `ThingDef_Building_CatastropheMissileSilo.xml`
****: `1.6/Defs/ThingDefs_Buildings/ThingDef_Building_CatastropheMissileSilo.xml`
****: `Building_CatastropheMissileSilo`
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BuildingBase">
<defName>CatastropheMissileSilo</defName>
<label></label>
<description></description>
<thingClass>ArachnaeSwarm.Building_CatastropheMissileSilo</thingClass>
<graphicData>
<texPath>Things/Building/Security/ShipMissileTurret</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(3,3)</drawSize>
</graphicData>
<size>(3,3)</size>
<statBases>
<MaxHitPoints>500</MaxHitPoints>
<WorkToBuild>8000</WorkToBuild>
<Mass>1000</Mass>
<Flammability>0.5</Flammability>
</statBases>
<costList>
<Steel>400</Steel>
<Plasteel>150</Plasteel>
<ComponentIndustrial>10</ComponentIndustrial>
<ComponentSpacer>4</ComponentSpacer>
</costList>
<comps>
<li Class="CompProperties_Power">
<compClass>CompPowerTrader</compClass>
<basePowerConsumption>500</basePowerConsumption>
</li>
<li Class="CompProperties_Refuelable">
<fuelConsumptionRate>10</fuelConsumptionRate> <!-- How much fuel per launch -->
<fuelCapacity>10.0</fuelCapacity>
<fuelFilter>
<thingDefs>
<li>Chemfuel</li>
</thingDefs>
</fuelFilter>
<showFuelGizmo>true</showFuelGizmo>
</li>
</comps>
<verbs>
<!-- This defines the weapon itself -->
<li>
<verbClass>ArachnaeSwarm.Verb_LaunchCatastropheMissile</verbClass>
<hasStandardCommand>false</hasStandardCommand>
<defaultProjectile>Projectile_CatastropheMissile</defaultProjectile>
</li>
</verbs>
<building>
<ai_combatDangerous>true</ai_combatDangerous>
</building>
<researchPrerequisites>
<li>ShipbuildingBasics</li> <!-- Placeholder research -->
</researchPrerequisites>
</ThingDef>
</Defs>
```
### 4.2 武器/投射物定义: `ThingDef_Projectile_CatastropheMissile.xml`
**路径**: `1.6/Defs/ThingDefs_Misc/ThingDef_Projectile_CatastropheMissile.xml`
**功能**: 定义导弹本身 (`Projectile_CatastropheMissile`) 和它作为投射物的行为 (`ThingDef` of `Projectile_CruiseMissile`)。这里是配置集束弹头和弹道参数的地方。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<ThingDef ParentName="BaseBullet">
<defName>Projectile_CatastropheMissile</defName>
<label>“天灾”巡航导弹</label>
<thingClass>ArachnaeSwarm.Projectile_CruiseMissile</thingClass>
<graphicData>
<texPath>Things/Projectile/Missile</texPath> <!-- Placeholder texture -->
<graphicClass>Graphic_Single</graphicClass>
</graphicData>
<projectile>
<damageDef>Bomb</damageDef>
<damageAmountBase>200</damageAmountBase>
<speed>10</speed> <!-- This speed is relative to the bezier curve, not linear -->
<explosionRadius>5.9</explosionRadius>
<soundExplode>MortarBomb_Explode</soundExplode>
</projectile>
<modExtensions>
<li Class="ArachnaeSwarm.CruiseMissileProperties">
<!-- Main Explosion -->
<customDamageDef>Bomb</customDamageDef>
<customDamageAmount>150</customDamageAmount>
<customExplosionRadius>5.9</customExplosionRadius>
<customSoundExplode>MortarBomb_Explode</customSoundExplode>
<!-- Sub Explosions (Cluster) -->
<useSubExplosions>true</useSubExplosions>
<subExplosionCount>8</subExplosionCount>
<subExplosionRadius>2.9</subExplosionRadius>
<subExplosionDamage>50</subExplosionDamage>
<subExplosionSpread>15</subExplosionSpread>
<subDamageDef>Bomb</subDamageDef>
<subSoundExplode>Fragment_Explode</subSoundExplode>
<!-- Trajectory Parameters -->
<bezierArcHeightFactor>0.05</bezierArcHeightFactor>
<bezierMinArcHeight>5</bezierMinArcHeight>
<bezierMaxArcHeight>20</bezierMaxArcHeight>
<bezierHorizontalOffsetFactor>0.1</bezierHorizontalOffsetFactor>
<bezierSideOffsetFactor>0.2</bezierSideOffsetFactor>
<bezierRandomOffsetScale>0.5</bezierRandomOffsetScale>
</li>
</modExtensions>
</ThingDef>
</Defs>
```
### 4.3 飞行物定义: `WorldObjectDef_CatastropheMissile.xml`
**路径**: `1.6/Defs/WorldObjectDefs/WorldObjectDef_CatastropheMissile.xml`
**功能**: 定义在世界地图上飞行的那个导弹实体。
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Defs>
<WorldObjectDef>
<defName>CatastropheMissile_Flying</defName>
<label>巡航导弹</label>
<worldObjectClass>ArachnaeSwarm.WorldObject_CatastropheMissile</worldObjectClass>
<texture>World/WorldObjects/Missile</texture> <!-- Placeholder texture -->
<expandingIcon>true</expandingIcon>
<canBeTargeted>false</canBeTargeted>
<canHavePlayerEnemyRelation>false</canHavePlayerEnemyRelation>
<useDynamicDrawer>true</useDynamicDrawer>
</WorldObjectDef>
</Defs>
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,743 @@
# Rimatomics 电磁炮战略打击系统技术文档
## 1. 引言
本文档旨在详细解析 Rimatomics Mod 中的电磁炮Railgun远程战略打击系统的实现机制。该系统允许玩家对世界地图上的任意已加载的地图进行精确的火力投送是游戏中一种强大的后期战略武器。
我们将通过分析其三个核心 C# 类:`Building_Railgun``Verb_Railgun``WorldObject_Sabot`,来深入理解其功能、设计模式以及类之间的交互。
## 2. 核心组件概览
整个系统由三个主要部分协同工作,各司其职:
* **`Building_Railgun`**: 炮塔建筑本身,是整个系统的用户交互入口和武器平台。它负责处理玩家的瞄准指令,管理武器的属性(如射程、精度),并发起射击流程。
* **`Verb_Railgun`**: 定义了电磁炮的“射击”这一具体动作。它能够智能区分常规的本地射击和需要跨地图飞行的远程打击,并根据不同模式执行相应的逻辑。
* **`WorldObject_Sabot`**: 一个在世界地图上存在的临时对象,用于模拟炮弹在飞向目标过程中的状态。它负责处理跨地图飞行、轨迹计算,并在抵达目标后触发最终的命中效果。
## 3. 类详解
### 3.1 `Building_Railgun`
`Building_Railgun` 是电磁炮塔的建筑类,继承自 `Building_EnergyWeapon`。它是玩家能直接看到并与之交互的实体。
#### 功能概述
此类作为系统的交互枢纽,主要负责以下功能:
* **提供玩家操作界面**:通过 Gizmo操作按钮让玩家启动“火控任务”。
* **处理目标选择流程**:引导玩家从世界地图选择目标区域,再到目标地图内选择精确弹着点。
* **管理武器状态与属性**:存储远程目标信息,并根据已安装的升级动态计算射程、散布等属性。
* **发起射击指令**在获取有效目标后命令自身的武器组件Verb开火。
#### 源代码
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Multiplayer.API;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace Rimatomics
{
[StaticConstructorOnStartup]
public class Building_Railgun : Building_EnergyWeapon
{
public override bool TurretBased
{
get
{
return true;
}
}
public override Vector3 TipOffset
{
get
{
Vector3 pos = this.DrawPos;
Vector3 vecOffset = new Vector3(0f, 1f, 5f);
vecOffset = vecOffset.RotatedBy(this.TurretRotation);
return pos + vecOffset;
}
}
public override bool CanFireWhileRoofed()
{
// For remote fire missions, cannot be roofed. For local targets, it's allowed.
if (this.longTargetInt == GlobalTargetInfo.Invalid)
{
return true;
}
return !base.Position.Roofed(base.Map);
}
public float RangeToWorldTarget
{
get
{
return (float)Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, this.longTarget.Tile, true, int.MaxValue, false);
}
}
public void TryChamberRound()
{
if (this.magazine.NullOrEmpty<ThingDef>())
{
return;
}
CompChangeableProjectile comp = this.gun.TryGetComp<CompChangeableProjectile>();
if (!comp.Loaded)
{
comp.LoadShell(this.magazine.FirstOrDefault<ThingDef>(), 1);
this.magazine.RemoveAt(0);
}
}
public override void MuzzleFlash()
{
Mote flash = (Mote)ThingMaker.MakeThing(Building_Railgun.Mote_RailgunMuzzleFlash, null);
flash.Scale = 5f;
flash.exactRotation = this.TurretRotation;
flash.exactPosition = this.TipOffset;
GenSpawn.Spawn(flash, base.Position, base.Map, WipeMode.Vanish);
Vector3 vecOffset = new Vector3(1f, 1f, -1f).RotatedBy(this.TurretRotation);
FleckMaker.ThrowSmoke(this.DrawPos + vecOffset, base.Map, 1.5f);
Vector3 vecOffset2 = new Vector3(-1f, 1f, -1f).RotatedBy(this.TurretRotation);
FleckMaker.ThrowSmoke(this.DrawPos + vecOffset2, base.Map, 1.5f);
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (Gizmo c in base.GetGizmos())
{
yield return c;
}
Command_Action launch = new Command_Action
{
defaultLabel = "critCommandFireMission".Translate(),
defaultDesc = "critCommandFireMissionDesc".Translate(),
icon = Building_Railgun.FireMissionTex,
action = new Action(this.StartChoosingDestination)
};
if (base.Spawned && base.Position.Roofed(base.Map))
{
launch.Disable("CannotFire".Translate() + ": " + "Roofed".Translate().CapitalizeFirst());
}
yield return launch;
}
private void StartChoosingDestination()
{
// Record all selected railguns to fire them simultaneously
this.selectedRailguns = Find.Selector.SelectedObjects.OfType<Building_Railgun>().ToList<Building_Railgun>();
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
int tile = base.Map.Tile;
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget), // Callback when target is chosen
false,
Building_Railgun.FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(tile, this.WorldRange, null), // Draws the range ring
null, null, null, false);
}
public static TargetingParameters ForFireMission()
{
// In multiplayer, disallow targeting mobile pawns to prevent desync issues.
if (MP.IsInMultiplayer)
{
return new TargetingParameters
{
canTargetPawns = false,
canTargetBuildings = false,
canTargetLocations = true
};
}
return new TargetingParameters
{
canTargetPawns = true,
canTargetBuildings = true,
canTargetLocations = true
};
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageRailgunTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, target.Tile, true, int.MaxValue, false);
if (distance > this.WorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return false;
}
MapParent mapParent = target.WorldObject as MapParent;
if (mapParent != null && mapParent.HasMap)
{
if (mapParent.Map == base.Map)
{
Messages.Message("MessageRailgunCantTargetMyMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
// --- Refactored Code Block ---
// This block handles the logic for fine-grained targeting within the destination map.
Map targetMap = mapParent.Map;
var originalMap = base.Map;
// Action to execute when targeting is finished or cancelled.
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap)
{
Current.Game.CurrentMap = originalMap;
}
};
// Switch to the target map to allow the player to pick a precise location.
Current.Game.CurrentMap = targetMap;
Find.Targeter.BeginTargeting(
Building_Railgun.ForFireMission(),
(LocalTargetInfo localTarget) => // Lambda for when a local target is selected
{
// Assign the fire mission to all selected railguns.
foreach (Building_Railgun railgun in this.selectedRailguns)
{
railgun.FireMission(targetMap.Tile, localTarget, targetMap.uniqueID);
}
},
null, // Action on highlight
onFinished, // Action on finish/cancel
Building_Railgun.FireMissionTex,
true);
return true;
}
else
{
Messages.Message("MessageRailgunNeedsMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
}
[SyncMethod(SyncContext.None)]
public void FireMission(int tile, LocalTargetInfo targ, int map)
{
if (!targ.IsValid)
{
this.longTargetInt = GlobalTargetInfo.Invalid;
return;
}
GlobalTargetInfo newtarget = targ.ToGlobalTargetInfo(Find.Maps.FirstOrDefault((Map x) => x.uniqueID == map));
int distance = Find.WorldGrid.TraversalDistanceBetween(base.Map.Tile, tile, true, int.MaxValue, false);
if (distance > this.WorldRange)
{
Messages.Message("MessageTargetBeyondMaximumRange".Translate(), this, MessageTypeDefOf.RejectInput, true);
return;
}
if (this.holdFire)
{
Messages.Message("MessageTurretWontFireBecauseHoldFire".Translate(this.def.label), this, MessageTypeDefOf.RejectInput, true);
return;
}
if (this.longTargetInt != newtarget)
{
this.longTargetInt = newtarget;
if (this.burstCooldownTicksLeft <= 0)
{
this.TryStartShootSomething();
}
SoundDefOf.TurretAcquireTarget.PlayOneShot(new TargetInfo(base.Position, base.Map, false));
}
}
public override float PulseSize
{
get
{
float f = base.GunProps.EnergyWep.PulseSizeScaled;
if (this.UG.HasUpgrade(DubDef.MEPS))
{
f *= 1.15f;
}
if (this.UG.HasUpgrade(DubDef.ERS))
{
f *= 0.85f; // Equivalent to f -= 0.15f * f
}
return f;
}
}
public override int WorldRange
{
get
{
// In space (SoS2 compatibility), range is unlimited.
if (base.Map != null && this.space != null && base.Map.Biome == this.space)
{
return 99999;
}
int range = base.GunProps.EnergyWep.WorldRange;
if (this.UG.HasUpgrade(DubDef.TargetingChip))
{
range += 10;
}
if (this.UG.HasUpgrade(DubDef.MEPS))
{
range += 10;
}
return range;
}
}
public int spread
{
get
{
int v = 6;
if (this.UG.HasUpgrade(DubDef.TargetingChip))
{
v -= 3;
}
return v;
}
}
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("Rimatomics/UI/FireMission", true);
public static ThingDef Mote_RailgunMuzzleFlash = ThingDef.Named("Mote_RailgunMuzzleFlash");
private List<Building_Railgun> selectedRailguns;
public BiomeDef space = DefDatabase<BiomeDef>.GetNamed("OuterSpaceBiome", false);
}
}
```
#### 关键方法和属性详解
* **`GetGizmos()`**: 为炮塔添加“火控任务” (Fire Mission) 按钮,这是远程打击流程的起点。如果炮塔被屋顶遮挡,该按钮会被禁用。
* **`StartChoosingDestination()`**: 响应按钮点击,将视角切换到世界地图,并启动一个带射程圈的目标选择器,让玩家选择目标地块。它支持多炮塔同时选择。
* **`ChoseWorldTarget(GlobalTargetInfo target)`**: 当玩家在世界地图上选择目标后的回调函数。它会进行一系列合法性验证(如射程、是否为当前地图等),如果通过,则将视角切换到目标地图,让玩家选择精确落点。
* **`FireMission(int tile, LocalTargetInfo targ, int map)`**: 这是目标选择的最后一步。它将最终的、精确的目标信息包含地块ID和地图内坐标存入 `longTargetInt` 变量,并立即尝试触发射击 (`TryStartShootSomething()`)。该方法支持多人游戏同步。
* **`WorldRange` (get)**: 一个计算属性返回炮塔在世界地图上的最大射程。基础射程定义在XML中并会受到 `TargetingChip``MEPS` 等升级的加成。
* **`spread` (get)**: 计算属性,返回炮弹的散布半径。`TargetingChip` 升级可以减小此值,提高精度。
* **`CanFireWhileRoofed()`**: 重写方法,规定在执行远程打击 (`longTargetInt` 有效) 时,炮塔不能处于屋顶之下。
### 3.2 `Verb_Railgun`
`Verb_Railgun` 继承自 `Verb_RimatomicsVerb`,它定义了电磁炮武器的“射击”这个核心动作。它不处理玩家交互,只负责执行射击逻辑。
#### 功能概述
这个类的主要职责是根据炮塔当前的目标状态(本地目标 vs 远程目标),决定执行哪种射击模式。
* **动态炮弹选择**: 根据 `CompChangeableProjectile` 组件的状态,决定是发射已装填的特殊炮弹还是默认炮弹。
* **射击模式分发**: 检查是否存在远程目标 (`longTarget`),如果存在,则执行“火控任务”流程;否则,执行标准的本地射击。
* **远程打击发起**: 在“火控任务”模式下,负责创建并初始化 `WorldObject_Sabot`,将其发射到世界地图。
* **本地打击后效**: 为本地射击附加额外的效果,如能量消耗、数据收集和屏幕震动。
#### 源代码
```csharp
using System;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace Rimatomics
{
[StaticConstructorOnStartup]
public class Verb_Railgun : Verb_RimatomicsVerb
{
public override ThingDef Projectile
{
get
{
ThingWithComps equipmentSource = base.EquipmentSource;
CompChangeableProjectile comp = (equipmentSource != null) ? equipmentSource.GetComp<CompChangeableProjectile>() : null;
if (comp != null && comp.Loaded)
{
return comp.Projectile;
}
return this.verbProps.defaultProjectile;
}
}
public override bool CanHitTargetFrom(IntVec3 root, LocalTargetInfo targ)
{
// If a long-range target is set, we can "hit" it regardless of local range.
if (base.GetWep.longTarget.IsValid)
{
return true;
}
return base.CanHitTargetFrom(root, targ);
}
protected bool TryCastFireMission()
{
Building_Railgun railgun = this.caster as Building_Railgun;
// Create the flying sabot object on the world map
WorldObject_Sabot sabot = (WorldObject_Sabot)WorldObjectMaker.MakeWorldObject(DefDatabase<WorldObjectDef>.GetNamed("Sabot", true));
sabot.railgun = railgun;
sabot.Tile = railgun.Map.Tile;
sabot.destinationTile = railgun.longTarget.Tile;
sabot.destinationCell = railgun.longTarget.Cell;
sabot.spread = railgun.spread;
sabot.Projectile = this.Projectile;
Find.WorldObjects.Add(sabot);
// Post-launch effects and data gathering
railgun.GatherData("PPCWeapon", 5f);
railgun.GatherData("PPCFireMission", 10f);
railgun.GatherData("PPCRailgun", 10f);
railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance);
railgun.MuzzleFlash();
Find.CameraDriver.shaker.SetMinShake(0.1f);
// Spawn a dummy projectile that flies off-map. This is for visual effect only.
Vector3 shellDirection = Vector3.forward.RotatedBy(railgun.TurretRotation);
IntVec3 outcell = (railgun.DrawPos + shellDirection * 500f).ToIntVec3();
Projectile projectile2 = (Projectile)GenSpawn.Spawn(this.Projectile, railgun.Position, this.caster.Map, WipeMode.Vanish);
projectile2.Launch(railgun, railgun.DrawPos, outcell, null, ProjectileHitFlags.None, false, base.EquipmentSource, null);
// Handle shell consumption
CompChangeableProjectile comp = base.EquipmentSource?.GetComp<CompChangeableProjectile>();
if (comp != null)
{
comp.Notify_ProjectileLaunched();
}
railgun.DissipateCharge(railgun.PulseSize);
return true;
}
public override bool TryCastShot()
{
Building_Railgun railgun = this.caster as Building_Railgun;
if (!railgun.top.TargetInSights)
{
return false;
}
if (this.Projectile == null)
{
return false;
}
bool shotResult;
// --- Shooting Mode Dispatch ---
if (railgun.longTarget.IsValid)
{
// Execute long-range fire mission
shotResult = this.TryCastFireMission();
}
else
{
// Execute standard local shot
shotResult = base.TryCastShot();
if (shotResult)
{
railgun.DissipateCharge(railgun.PulseSize);
railgun.GatherData("PPCWeapon", 5f);
railgun.GatherData("PPCRailgun", 10f);
railgun.PrototypeBang(railgun.GunProps.EnergyWep.PrototypeFailureChance);
railgun.MuzzleFlash();
Find.CameraDriver.shaker.SetMinShake(0.1f);
}
}
// Chamber the next round after firing
railgun.TryChamberRound();
return shotResult;
}
}
}
```
#### 关键方法和属性详解
* **`Projectile` (get)**: 这是一个动态属性,用于获取当前应该发射的炮弹类型。它会优先返回 `CompChangeableProjectile` 中已装填的炮弹,如果未装填,则返回在 XML 中定义的默认炮弹。
* **`CanHitTargetFrom(...)`**: 重写了基类方法。当炮塔被赋予了一个远程目标 (`longTarget.IsValid`) 时,该方法直接返回 `true`,绕过了所有常规的射程和视线检查,确保远程打击流程可以启动。
* **`TryCastShot()`**: 这是射击动作的入口点。它的核心逻辑是检查 `railgun.longTarget` 是否有效。
* 如果**有效**,说明是远程打击任务,它便调用 `TryCastFireMission()`
* 如果**无效**,说明是常规的本地瞄准,它就调用基类的 `base.TryCastShot()` 来发射普通炮弹,并附加一系列开火后效。
* **`TryCastFireMission()`**: 这是发起远程打击的核心。它不直接生成命中目标的炮弹,而是:
1. 创建一个 `WorldObject_Sabot` 实例。
2. 将目标地块、精确坐标、炮弹类型、散布等关键信息从 `Building_Railgun` 传递给 `sabot` 对象。
3.`sabot` 添加到世界对象管理器 (`Find.WorldObjects.Add(sabot)`),让其开始在世界地图上“飞行”。
4. 触发炮口闪光、能量消耗、屏幕震动等本地开火效果。
5. 生成一个飞向地图外的“虚拟”炮弹,仅用于视觉表现,它不会造成任何伤害。
### 3.3 `WorldObject_Sabot`
`WorldObject_Sabot` 继承自 `WorldObject`,是远程打击流程中的“飞行”阶段的执行者。它是一个临时的、在世界地图上存在的实体,模拟了炮弹从发射点到目标点的飞行过程。
#### 功能概述
此类完全独立于发射它的炮塔,其核心职责是在世界地图上完成一段旅程,并在抵达终点时触发命中效果。
* **飞行轨迹模拟**: 通过 `DrawPos` 属性和 `Tick` 方法,平滑地计算并更新自身在世界地图上的位置,实现飞行-动画效果。
* **状态持久化**: 通过 `ExposeData` 方法保存所有关键信息(如起点、终点、飞行进度、炮弹类型),确保在游戏存读档后飞行可以继续。
* **命中触发**: 在飞行到达终点后,`Arrived` 方法负责在目标地图上生成真正的 `Projectile`(炮弹),并让其从地图边缘发射,命中最终的精确弹着点。
* **自我销毁**: 完成命中逻辑后,将自身从世界对象管理器中移除。
* **Mod兼容性**: 能够识别并处理 "Save Our Ship 2" Mod 中的轨道飞船,实现地对空、空对地和空对空打击。
#### 源代码
```csharp
using System;
using System.Linq;
using RimWorld.Planet;
using UnityEngine;
using Verse;
namespace Rimatomics
{
public class WorldObject_Sabot : WorldObject
{
private Vector3 Start
{
get
{
Vector3 startPos = Find.WorldGrid.GetTileCenter(this.initialTile);
// SoS2 compatibility: if a ship is at the tile, use its position.
if (HarmonyPatches.SoS)
{
WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o =>
(o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.initialTile);
if (ship != null)
{
startPos = ship.DrawPos;
}
}
return startPos;
}
}
private Vector3 End
{
get
{
Vector3 endPos = Find.WorldGrid.GetTileCenter(this.destinationTile);
// SoS2 compatibility
if (HarmonyPatches.SoS)
{
WorldObject ship = Find.World.worldObjects.AllWorldObjects.FirstOrDefault(o =>
(o.def.defName.Equals("ShipOrbiting") || o.def.defName.Equals("SiteSpace")) && o.Tile == this.destinationTile);
if (ship != null)
{
endPos = ship.DrawPos;
}
}
return endPos;
}
}
public override Vector3 DrawPos
{
get
{
// Slerp for a smooth curve over the planet's surface
return Vector3.Slerp(this.Start, this.End, this.traveledPct);
}
}
private float TraveledPctStepPerTick
{
get
{
Vector3 start = this.Start;
Vector3 end = this.End;
if (start == end) return 1f;
float distance = GenMath.SphericalDistance(start.normalized, end.normalized);
if (distance == 0f) return 1f;
// Travel speed is constant
return TravelSpeed / distance;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look<int>(ref this.destinationTile, "destinationTile", 0, false);
Scribe_Values.Look<IntVec3>(ref this.destinationCell, "destinationCell", default(IntVec3), false);
Scribe_Values.Look<bool>(ref this.arrived, "arrived", false, false);
Scribe_Values.Look<int>(ref this.initialTile, "initialTile", 0, false);
Scribe_Values.Look<float>(ref this.traveledPct, "traveledPct", 0f, false);
Scribe_Defs.Look<ThingDef>(ref this.Projectile, "Projectile");
Scribe_References.Look<Thing>(ref this.railgun, "railgun");
Scribe_Values.Look<int>(ref this.spread, "spread", 1, false);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = base.Tile;
}
public override void Tick()
{
base.Tick();
this.traveledPct += this.TraveledPctStepPerTick;
if (this.traveledPct >= 1f)
{
this.traveledPct = 1f;
this.Arrived();
}
}
private void Arrived()
{
if (this.arrived) return;
this.arrived = true;
Map map = Current.Game.FindMap(this.destinationTile);
if (map != null)
{
// Spawn the projectile at the edge of the map
IntVec3 entryCell = new IntVec3(CellRect.WholeMap(map).Width / 2, 0, CellRect.WholeMap(map).maxZ);
Projectile projectile = (Projectile)GenSpawn.Spawn(this.Projectile, entryCell, map, WipeMode.Vanish);
// Find a random cell near the target destination within the spread radius
IntVec3 finalDestination;
CellFinder.TryFindRandomCellNear(this.destinationCell, map, this.spread, null, out finalDestination, -1);
// Launch the projectile to the final destination
projectile.Launch(this.railgun, finalDestination, finalDestination, ProjectileHitFlags.IntendedTarget, false, null);
}
// Remove self from the world
Find.WorldObjects.Remove(this);
}
private const float TravelSpeed = 0.0001f;
private bool arrived;
public IntVec3 destinationCell = IntVec3.Invalid;
public int destinationTile = -1;
private int initialTile = -1;
public ThingDef Projectile;
public Thing railgun;
public int spread = 1;
private float traveledPct;
}
}
```
#### 关键方法和属性详解
* **`Start` / `End` (get)**: 这两个属性负责计算飞行的起点和终点坐标。它们通过检查 `HarmonyPatches.SoS` 来判断是否加载了 "Save Our Ship 2" Mod如果加载了则会尝试获取轨道上飞船的位置作为起点/终点从而实现与该Mod的无缝兼容。
* **`DrawPos` (get)**: 重写属性,用于在世界地图上渲染该对象。它使用 `Vector3.Slerp`(球面线性插值)在起点和终点之间进行插值,根据 `traveledPct`(已飞行百分比)计算出当前帧应该在的位置,从而形成一条平滑的弧形飞行轨迹。
* **`Tick()`**: 游戏引擎为每个世界对象调用的更新方法。它在每一帧增加 `traveledPct` 来推进飞行进度。当 `traveledPct` 达到1时调用 `Arrived()`
* **`Arrived()`**: 这是飞行结束、触发命中的核心方法。它首先检查目标地块是否存在一个已加载的地图。如果存在,它会在该地图的边缘生成 `Projectile` 实体,并根据 `spread`(散布)在目标点附近随机一个最终落点,然后调用炮弹的 `Launch()` 方法完成最后的攻击。无论地图是否存在,它最终都会将自己从世界上移除。
* **`ExposeData()`**: 保证了该飞行物体的所有状态(起点、终点、进度、炮弹类型等)都能被正确地保存和加载。
## 4. 系统工作流程
现在我们将三个核心组件联系起来,详细描述从玩家点击按钮到炮弹命中的完整工作流程。
### 4.1 交互时序图
下面的 Mermaid 时序图直观地展示了不同对象之间的交互顺序。
```mermaid
sequenceDiagram
participant Player as 玩家
participant Railgun as Building_Railgun
participant Verb as Verb_Railgun
participant Sabot as WorldObject_Sabot
participant Projectile as 最终炮弹
Player->>Railgun: 点击 "Fire Mission" 按钮
activate Railgun
Railgun->>Player: 显示世界地图和射程圈
Player->>Railgun: 1. 选择世界目标 (地块)
Player->>Railgun: 2. 选择地图内目标 (坐标)
Railgun->>Railgun: 调用 FireMission() 设置 longTarget
Railgun->>Verb: 调用 TryStartShootSomething()
deactivate Railgun
activate Verb
Verb->>Verb: TryCastShot() 检测到 longTarget
Verb->>Verb: 调用 TryCastFireMission()
Verb->>Sabot: new WorldObject_Sabot()
activate Sabot
Verb->>Sabot: 传递目标信息、炮弹类型等
Verb-->>Player: 触发炮口闪光和声音
deactivate Verb
loop 飞行过程 (每Tick)
Sabot->>Sabot: 更新 traveledPct (飞行进度)
end
Sabot->>Sabot: Arrived() - 到达目标
Sabot->>Projectile: GenSpawn.Spawn(炮弹)
activate Projectile
Sabot->>Projectile: Launch() - 飞向最终落点
Projectile-->>Player: 爆炸和伤害效果
deactivate Projectile
Sabot->>Sabot: Find.WorldObjects.Remove(this)
deactivate Sabot
```
### 4.2 步骤分解
1. **启动与目标选择 (玩家 -> `Building_Railgun`)**
* 玩家选中一门或多门 `Building_Railgun` 并点击其 "Fire Mission" Gizmo。
* `GetGizmos` 触发 `StartChoosingDestination` 方法,视角切换至世界地图,并显示最大射程圈。
* 玩家在世界地图上选择一个目标地块。
* `ChoseWorldTarget` 方法被回调。在通过一系列验证后,视角切换到目标地图。
* 玩家在目标地图上选择一个精确的弹着点。
2. **下达指令 (`Building_Railgun` -> `Verb_Railgun`)**
* 当精确弹着点被选定后,`ChoseWorldTarget` 方法会为所有被选中的炮塔调用 `FireMission` 方法。
* `FireMission` 将包含地块和坐标的 `GlobalTargetInfo` 存入炮塔的 `longTarget` 变量中。
* `FireMission` 随即调用 `TryStartShootSomething()`这会启动炮塔的射击冷却计时器并最终触发其Verb组件。
3. **发射与创建飞行物 (`Verb_Railgun` -> `WorldObject_Sabot`)**
* 炮塔的 `Verb_Railgun` 组件的 `TryCastShot` 方法被调用。
* `TryCastShot` 检测到 `longTarget` 是有效的,因此它不会执行常规射击,而是调用 `TryCastFireMission`
* `TryCastFireMission` 创建一个 `WorldObject_Sabot` 的实例,并将目标信息、炮弹定义、散布等关键数据从 `Building_Railgun` 复制到这个新实例中。
* `WorldObject_Sabot` 被添加到 `Find.WorldObjects` 管理器中,正式开始其生命周期。
* 同时,`Verb_Railgun` 在本地触发开火的视觉和听觉效果。
4. **跨地图飞行 (`WorldObject_Sabot`)**
* `WorldObject_Sabot` 作为一个独立的世界对象,其 `Tick` 方法被游戏引擎在每一帧调用。
* `Tick` 方法不断更新 `traveledPct` 属性,模拟飞行进度。
*`DrawPos` 属性根据 `traveledPct` 在世界地图上平滑地渲染出飞行轨迹。
5. **抵达与命中 (`WorldObject_Sabot` -> `Projectile`)**
*`traveledPct` 达到100%时,`Arrived` 方法被调用。
* `Arrived` 检查目标地块的地图是否已加载。
* 如果地图已加载,它会在地图边缘 `GenSpawn.Spawn` 一个真正的 `Projectile`(最终炮弹)。
* 根据从 `Building_Railgun` 继承来的 `spread` 值,在玩家指定的精确落点附近随机一个最终弹着点。
* 调用 `Projectile.Launch()`,使其从地图边缘飞向并命中最终弹着点,产生爆炸和伤害。
* 最后,`WorldObject_Sabot` 调用 `Find.WorldObjects.Remove(this)` 将自己从世界上移除,完成其使命。
## 5. 总结
Rimatomics的电磁炮系统是一个设计精良的远程打击模块。它通过将**交互(Building)**、**动作(Verb)**和**飞行(WorldObject)**三个阶段清晰地分离到不同的类中,实现了高度的内聚和低耦合。这种设计不仅使得代码逻辑清晰、易于维护,还通过 `WorldObject` 机制优雅地解决了跨地图状态同步和持久化的问题并为兼容其他Mod如Save Our Ship 2留出了接口。

View File

@@ -1,7 +1,16 @@
{
"Version": 1,
"WorkspaceRootPath": "E:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\",
"Documents": [],
"WorkspaceRootPath": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\pawn_comps\\ara_fighter_invisible\\compproperties_fighterinvisible.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:pawn_comps\\ara_fighter_invisible\\compproperties_fighterinvisible.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|d:\\steamlibrary\\steamapps\\common\\rimworld\\mods\\arachnaeswarm\\source\\arachnaeswarm\\pawn_comps\\ara_fighter_invisible\\compfighterinvisible.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{EAE0DB6B-E282-C812-7F5A-6D13E9D24581}|ArachnaeSwarm.csproj|solutionrelative:pawn_comps\\ara_fighter_invisible\\compfighterinvisible.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
@@ -9,11 +18,37 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": -1,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "CompFighterInvisible.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_Fighter_Invisible\\CompFighterInvisible.cs",
"RelativeDocumentMoniker": "Pawn_Comps\\ARA_Fighter_Invisible\\CompFighterInvisible.cs",
"ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_Fighter_Invisible\\CompFighterInvisible.cs",
"RelativeToolTip": "Pawn_Comps\\ARA_Fighter_Invisible\\CompFighterInvisible.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-09-22T01:22:42.97Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "CompProperties_FighterInvisible.cs",
"DocumentMoniker": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_Fighter_Invisible\\CompProperties_FighterInvisible.cs",
"RelativeDocumentMoniker": "Pawn_Comps\\ARA_Fighter_Invisible\\CompProperties_FighterInvisible.cs",
"ToolTip": "D:\\SteamLibrary\\steamapps\\common\\RimWorld\\Mods\\ArachnaeSwarm\\Source\\ArachnaeSwarm\\Pawn_Comps\\ARA_Fighter_Invisible\\CompProperties_FighterInvisible.cs",
"RelativeToolTip": "Pawn_Comps\\ARA_Fighter_Invisible\\CompProperties_FighterInvisible.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAwAAAAyAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2025-09-22T01:22:42.218Z",
"EditorCaption": ""
}
]
}

View File

@@ -154,6 +154,7 @@
<Compile Include="Hediffs\WULA_HediffDamgeShield\DRMDamageShield.cs" />
<Compile Include="Hediffs\WULA_HediffDamgeShield\Hediff_DamageShield.cs" />
<Compile Include="MainHarmony.cs" />
<Compile Include="CompAndPatch_GiveHediffOnShot.cs" />
<Compile Include="Pawn_Comps\ARA_AutoMechCarrier\CompAutoMechCarrier.cs" />
<Compile Include="Pawn_Comps\ARA_AutoMechCarrier\CompProperties_AutoMechCarrier.cs" />
<Compile Include="Pawn_Comps\ARA_AutoMechCarrier\PawnProductionEntry.cs" />
@@ -161,6 +162,8 @@
<Compile Include="Pawn_Comps\ARA_CompHediffGiver\CompProperties_HediffGiver.cs" />
<Compile Include="Pawn_Comps\ARA_CompMilkableArachnae\CompMilkableArachnae.cs" />
<Compile Include="Pawn_Comps\ARA_CompMilkableArachnae\CompProperties_MilkableArachnae.cs" />
<Compile Include="Pawn_Comps\ARA_Fighter_Invisible\CompFighterInvisible.cs" />
<Compile Include="Pawn_Comps\ARA_Fighter_Invisible\CompProperties_FighterInvisible.cs" />
<Compile Include="Pawn_Comps\ARA_Flight\CompPawnFlight.cs" />
<Compile Include="Pawn_Comps\ARA_Flight\CompProperties_PawnFlight.cs" />
<Compile Include="Pawn_Comps\ARA_Flight\Pawn_FlightTrackerPatches.cs" />
@@ -199,11 +202,16 @@
<Compile Include="Verbs\Verb_ShootMeltBeam.cs" />
<Compile Include="Verbs\Verb_ShootShotgun.cs" />
<Compile Include="Verbs\VerbProperties_Excalibur.cs" />
<Compile Include="Verbs\Verb_ShootSprayMulti.cs" />
<Compile Include="Verbs\WeaponStealBeam\Verb_ShootWeaponStealBeam.cs" />
<Compile Include="Verbs\WeaponStealBeam\VerbProperties_WeaponStealBeam.cs" />
<Compile Include="Building_ARANutrientDispenser.cs" />
<Compile Include="ARAFoodDispenserProperties.cs" />
<Compile Include="Patch_DispenserFoodSearch.cs" />
<Compile Include="Buildings\Building_CatastropheMissileSilo.cs" />
<Compile Include="World\WorldObject_CatastropheMissile.cs" />
<Compile Include="HarmonyPatches\Patch_ForceTargetable.cs" />
<Compile Include="Comps\CompForceTargetable.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="Wormhole\Building_WormholePortal_A.cs" />

View File

@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Verse;
using Verse.AI;
using Verse.Sound;
using RimWorld;
using RimWorld.Planet;
namespace ArachnaeSwarm
{
[StaticConstructorOnStartup]
public class Building_CatastropheMissileSilo : Building_TurretGun
{
public GlobalTargetInfo longTarget;
public static readonly Texture2D FireMissionTex = ContentFinder<Texture2D>.Get("UI/Commands/Attack", true);
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
if (!respawningAfterLoad)
{
this.longTarget = GlobalTargetInfo.Invalid;
}
}
public override void ExposeData()
{
base.ExposeData();
Scribe_TargetInfo.Look(ref this.longTarget, "longTarget");
}
protected override void Tick()
{
// Base tick handles all local targeting, cooldowns, and fuel consumption via XML.
base.Tick();
// If a local target is active, prevent remote targeting.
if (this.forcedTarget.IsValid && this.longTarget.IsValid)
{
this.longTarget = GlobalTargetInfo.Invalid;
}
// If a remote target is set and the turret is ready, fire.
// The base.Tick() cooldown handling prevents this from running if a local shot was just fired.
if (this.longTarget.IsValid && this.burstCooldownTicksLeft <= 0 && base.Active && CanFireGlobal(out _))
{
this.FireMission(this.longTarget);
}
}
public override string GetInspectString()
{
StringBuilder sb = new StringBuilder(base.GetInspectString());
if (burstCooldownTicksLeft > 0)
{
if (sb.Length > 0) sb.AppendLine();
sb.Append("Cooldown".Translate() + ": " + this.burstCooldownTicksLeft.ToStringTicksToPeriod());
}
if (this.longTarget.IsValid)
{
if (sb.Length > 0) sb.AppendLine();
sb.Append("RemoteTargetSet".Translate(this.longTarget.Label));
}
return sb.ToString();
}
public override IEnumerable<Gizmo> GetGizmos()
{
foreach (var g in base.GetGizmos())
{
yield return g;
}
Command_Action fireGlobal = new Command_Action
{
defaultLabel = "CommandFireGlobal".Translate(),
defaultDesc = "CommandFireGlobalDesc".Translate(),
icon = FireMissionTex,
action = new Action(StartChoosingDestination)
};
if (!CanFireGlobal(out string reason))
{
fireGlobal.Disable(reason);
}
if (this.forcedTarget.IsValid)
{
fireGlobal.Disable("LocalTargetForced".Translate());
}
yield return fireGlobal;
if (this.longTarget.IsValid)
{
Command_Action clearRemote = new Command_Action
{
defaultLabel = "CommandClearRemoteTarget".Translate(),
defaultDesc = "CommandClearRemoteTargetDesc".Translate(),
icon = ContentFinder<Texture2D>.Get("UI/Designators/Cancel"),
action = () =>
{
this.longTarget = GlobalTargetInfo.Invalid;
}
};
yield return clearRemote;
}
}
public void FireMission(GlobalTargetInfo target)
{
if (!CanFireGlobal(out _)) return;
WorldObject_CatastropheMissile missile = (WorldObject_CatastropheMissile)WorldObjectMaker.MakeWorldObject(
DefDatabase<WorldObjectDef>.GetNamed("CatastropheMissile_Flying")
);
missile.Tile = this.Map.Tile;
missile.destinationTile = target.Tile;
missile.destinationCell = target.Cell;
missile.Projectile = DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile");
Find.WorldObjects.Add(missile);
Vector3 shellDirection = Vector3.forward.RotatedBy(this.top.CurRotation);
IntVec3 outcell = (this.DrawPos + shellDirection * 500f).ToIntVec3();
Projectile_CruiseMissile dummy = (Projectile_CruiseMissile)GenSpawn.Spawn(DefDatabase<ThingDef>.GetNamed("Projectile_CatastropheMissile_Fake"), this.Position, this.Map);
dummy?.Launch(this, this.DrawPos, new LocalTargetInfo(outcell), new LocalTargetInfo(outcell), ProjectileHitFlags.None);
var refuelableComp = this.TryGetComp<CompRefuelable>();
if(refuelableComp != null)
{
refuelableComp.ConsumeFuel(1);
}
SoundDef.Named("RocketLaunch").PlayOneShot(new TargetInfo(this.Position, this.Map));
this.BurstComplete();
}
private bool CanFireGlobal(out string reason)
{
var refuelableComp = this.TryGetComp<CompRefuelable>();
if (refuelableComp != null && !refuelableComp.HasFuel)
{
reason = "NoFuel".Translate().CapitalizeFirst();
return false;
}
reason = "";
return true;
}
private void StartChoosingDestination()
{
CameraJumper.TryJump(CameraJumper.GetWorldTarget(this), CameraJumper.MovementMode.Pan);
Find.WorldSelector.ClearSelection();
Find.WorldTargeter.BeginTargeting(
new Func<GlobalTargetInfo, bool>(this.ChoseWorldTarget),
true,
FireMissionTex,
true,
() => GenDraw.DrawWorldRadiusRing(this.Map.Tile, 99999),
null, null, null, true);
}
private bool ChoseWorldTarget(GlobalTargetInfo target)
{
if (!target.IsValid)
{
Messages.Message("MessageTargetInvalid".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
if (target.Tile == this.Map.Tile)
{
Messages.Message("Cannot target own map for global strike.", MessageTypeDefOf.RejectInput, true);
return false;
}
if (target.WorldObject is MapParent mapParent && mapParent.HasMap)
{
var originalMap = this.Map;
Action onFinished = () => {
if (Current.Game.CurrentMap != originalMap) Current.Game.CurrentMap = originalMap;
};
Current.Game.CurrentMap = mapParent.Map;
Find.Targeter.BeginTargeting(new TargetingParameters
{
canTargetLocations = true,
canTargetPawns = true,
canTargetBuildings = true
},
(LocalTargetInfo localTarget) =>
{
if (localTarget.HasThing)
{
this.longTarget = new GlobalTargetInfo(localTarget.Thing);
}
else
{
this.longTarget = new GlobalTargetInfo(localTarget.Cell, mapParent.Map);
}
this.forcedTarget = LocalTargetInfo.Invalid;
},
null, onFinished, FireMissionTex, true);
return true;
}
else
{
Messages.Message("MessageTargetMustBeMap".Translate(), MessageTypeDefOf.RejectInput, true);
return false;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using HarmonyLib;
using RimWorld;
using Verse;
namespace ArachnaeSwarm
{
// 1. 定义CompProperties来存储我们的数据
public class CompProperties_GiveHediffOnShot : CompProperties
{
public HediffDef hediffDef;
public float severityToAdd = 0.1f;
public CompProperties_GiveHediffOnShot()
{
compClass = typeof(CompGiveHediffOnShot);
}
}
// 2. 创建一个简单的Comp来挂载到武器上它只负责持有数据
public class CompGiveHediffOnShot : ThingComp
{
public CompProperties_GiveHediffOnShot Props => (CompProperties_GiveHediffOnShot)props;
}
// 3. 创建Harmony补丁
// 补丁目标为 Verb_Shoot.TryCastShot。这是所有射击动作的通用入口。
[HarmonyPatch(typeof(Verb_Shoot), "TryCastShot")]
public static class Patch_Verb_Shoot_TryCastShot
{
// 使用[HarmonyPostfix]特性来创建一个在原方法执行后运行的补丁
// 添加一个bool类型的返回值`__result`,它代表了原方法的返回值
public static void Postfix(Verb_Shoot __instance, bool __result)
{
// __result 是原方法 TryCastShot 的返回值。
// 如果 __result 为 false意味着射击动作因某些原因如目标无效、没有弹药失败了我们就不应该添加Hediff。
if (!__result)
{
return;
}
// __instance是原方法的实例对象也就是那个Verb_Shoot
// 检查这个Verb是否来源于一个装备武器
if (__instance.EquipmentSource == null || __instance.CasterPawn == null)
{
return;
}
// 尝试从武器上获取我们自定义的Comp
CompGiveHediffOnShot comp = __instance.EquipmentSource.GetComp<CompGiveHediffOnShot>();
if (comp == null)
{
return;
}
// 检查XML中是否配置了hediffDef
if (comp.Props.hediffDef == null)
{
Log.ErrorOnce($"[ArachnaeSwarm] CompGiveHediffOnShot on {__instance.EquipmentSource.def.defName} has null hediffDef.", __instance.EquipmentSource.def.GetHashCode());
return;
}
// 为射击者CasterPawn添加或增加Hediff的严重性
Hediff hediff = __instance.CasterPawn.health.GetOrAddHediff(comp.Props.hediffDef);
hediff.Severity += comp.Props.severityToAdd;
// 检查Hediff是否带有HediffComp_Disappears组件
HediffComp_Disappears disappearsComp = hediff.TryGetComp<HediffComp_Disappears>();
if (disappearsComp != null)
{
// 如果有,则调用正确的方法重置它的消失计时器
disappearsComp.ResetElapsedTicks();
}
}
}
}

View File

@@ -0,0 +1,18 @@
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_ForceTargetable : CompProperties
{
public CompProperties_ForceTargetable()
{
this.compClass = typeof(CompForceTargetable);
}
}
public class CompForceTargetable : ThingComp
{
// This component doesn't need any specific logic.
// Its mere presence on a turret is checked by the Harmony patch.
}
}

View File

@@ -0,0 +1,23 @@
using HarmonyLib;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
[HarmonyPatch(typeof(Building_TurretGun), "get_CanSetForcedTarget")]
public static class Patch_Building_TurretGun_CanSetForcedTarget
{
public static void Postfix(Building_TurretGun __instance, ref bool __result)
{
if (__result)
{
return;
}
if (__instance.GetComp<CompForceTargetable>() != null && __instance.Faction == Faction.OfPlayer)
{
__result = true;
}
}
}
}

View File

@@ -0,0 +1,71 @@
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI.Group;
using Verse.Sound;
using static HarmonyLib.Code;
namespace ArachnaeSwarm
{
public class CompFighterInvisible : ThingComp
{
public CompProperties_FighterInvisible Props => (CompProperties_FighterInvisible)props;
[Unsaved(false)]
private HediffComp_Invisibility invisibility;
private int lastDetectedTick = -99999;
private Pawn Sightstealer => (Pawn)parent;
public HediffDef GetTargetInvisibilityDef()
{
return Props.InvisibilityDef;
}
//检测是否拥有定义的InvisibilityDef
private HediffComp_Invisibility Invisibility => invisibility ?? (invisibility = Sightstealer.health.hediffSet.GetFirstHediffOfDef(GetTargetInvisibilityDef())?.TryGetComp<HediffComp_Invisibility>());
//用于在存档储存lastDetectedTick
public override void PostExposeData()
{
Scribe_Values.Look(ref lastDetectedTick, "lastDetectedTick", 0);
}
public override void CompTick()
{
//蹒跚怪不会隐身
if (Sightstealer.IsShambler)
{
return;
}
//如果没有定义的InvisibilityDef则返回false
if (Invisibility == null)
{
return;
}
if (!Sightstealer.Spawned)
{
return;
}
//进行隐身检查,如果超过定义的隐身恢复时间则切换为隐身
if (Sightstealer.IsHashIntervalTick(7))
{
if (Find.TickManager.TicksGame > lastDetectedTick + Props.stealthCooldownTicks)
{
Invisibility.BecomeInvisible();
}
}
}
//在使用verb的时候会强制显现
public override void Notify_UsedVerb(Pawn pawn, Verb verb)
{
base.Notify_UsedVerb(pawn, verb);
if (!Sightstealer.IsShambler)
{
Invisibility.BecomeVisible();
lastDetectedTick = Find.TickManager.TicksGame;
}
}
}
}

View File

@@ -0,0 +1,31 @@
using RimWorld;
using System.Collections.Generic;
using Verse;
namespace ArachnaeSwarm
{
public class CompProperties_FighterInvisible : CompProperties
{
public float BaseVisibleRadius = 14f;
public int UndetectedTimeout = 120;
public int CheckDetectedIntervalTicks = 7;
public float FirstDetectedRadius = 30f;
public int RevealedLetterDelayTicks = 6;
public int AmbushCallMTBTicks = 600;
//修改一个可定义的提供隐身的hediff
public HediffDef InvisibilityDef;
//隐身冷却
public int stealthCooldownTicks = 1200;
public CompProperties_FighterInvisible()
{
compClass = typeof(CompFighterInvisible);
}
}
}

View File

@@ -8,6 +8,7 @@ namespace ArachnaeSwarm
{
public class CruiseMissileProperties : DefModExtension
{
public bool isDummy = false;
public DamageDef customDamageDef;
public int customDamageAmount = 5;
public float customExplosionRadius = 1.1f;
@@ -38,11 +39,19 @@ namespace ArachnaeSwarm
private Vector3 Randdd;
private Vector3 position2;
public Vector3 ExPos;
public bool isDummy = false;
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref isDummy, "isDummy", false);
}
public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);
settings = def.GetModExtension<CruiseMissileProperties>() ?? new CruiseMissileProperties();
this.isDummy = settings.isDummy;
}
private void RandFactor()
@@ -113,6 +122,11 @@ namespace ArachnaeSwarm
var map = base.Map;
base.Impact(hitThing, blockedByShield);
if (isDummy)
{
return;
}
DoExplosion(
base.Position,
map,

View File

@@ -0,0 +1,134 @@
using RimWorld;
using System.Collections.Generic;
using UnityEngine;
using Verse;
using Verse.Sound;
namespace ArachnaeSwarm
{
public class Verb_ShootSprayMulti : Verb_Shoot
{
public VerbProperties_ShootSprayMulti SprayProps => verbProps as VerbProperties_ShootSprayMulti;
protected override bool TryCastShot()
{
if (currentTarget.HasThing && currentTarget.Thing.Map != caster.Map)
{
return false;
}
List<IntVec3> cells = AffectedCells(currentTarget);
if (cells.Count == 0)
{
return false;
}
// A single "shot" from the verb will fire projectiles at all affected cells.
// The base verb's burst fire mechanism will handle firing multiple "shots".
bool shotFired = false;
foreach (var cell in cells)
{
if (CanHitTarget(cell))
{
ShootLine shootLine;
if (TryFindShootLineFromTo(caster.Position, cell, out shootLine))
{
Projectile projectile = (Projectile)GenSpawn.Spawn(verbProps.defaultProjectile, shootLine.Source, caster.Map);
// Corrected Launch call without targetCover
projectile.Launch(caster, caster.DrawPos, cell, cell, ProjectileHitFlags.IntendedTarget, equipment: (caster as Pawn)?.equipment?.Primary);
shotFired = true;
}
}
}
if (shotFired)
{
// Corrected sound calls based on MCP knowledge
if (verbProps.soundCast != null)
{
verbProps.soundCast.PlayOneShot(SoundInfo.InMap(new TargetInfo(caster.Position, caster.Map)));
}
if (verbProps.soundCastTail != null)
{
verbProps.soundCastTail.PlayOneShotOnCamera();
}
if (caster is Pawn pawn)
{
if (pawn.records != null)
{
pawn.records.Increment(RecordDefOf.ShotsFired);
}
if (pawn.skills != null)
{
pawn.skills.Learn(SkillDefOf.Shooting, 0.5f);
}
}
}
return shotFired;
}
// This logic is adapted from CompAbilityEffect_SprayLiquidMulti
private List<IntVec3> AffectedCells(LocalTargetInfo target)
{
List<Pair<IntVec3, float>> tmpCellDots = new List<Pair<IntVec3, float>>();
List<IntVec3> tmpCells = new List<IntVec3>();
tmpCellDots.Clear();
tmpCells.Clear();
tmpCellDots.Add(new Pair<IntVec3, float>(target.Cell, 999f));
int numCellsToHit = SprayProps?.numCellsToHit ?? 1;
if (numCellsToHit > 1)
{
Vector3 vector = caster.Position.ToVector3Shifted().Yto0();
Vector3 vector2 = target.Cell.ToVector3Shifted().Yto0();
IntVec3[] array;
int num;
if (numCellsToHit < 10)
{
array = GenAdj.AdjacentCells;
num = 8;
}
else
{
array = GenRadial.RadialPattern;
num = numCellsToHit + 5;
}
for (int i = 0; i < num; i++)
{
IntVec3 first = target.Cell + array[i];
Vector3 vector3 = first.ToVector3Shifted().Yto0();
float second = Vector3.Dot((vector3 - vector).normalized, (vector3 - vector2).normalized);
tmpCellDots.Add(new Pair<IntVec3, float>(first, second));
}
tmpCellDots.SortByDescending(x => x.Second);
}
Map map = caster.Map;
int num2 = Mathf.Min(tmpCellDots.Count, numCellsToHit);
for (int j = 0; j < num2; j++)
{
IntVec3 first2 = tmpCellDots[j].First;
if (!first2.InBounds(map)) continue;
if (first2.Filled(map))
{
Building_Door door = first2.GetDoor(map);
if (door == null || !door.Open) continue;
}
if (TryFindShootLineFromTo(caster.Position, first2, out var _))
{
tmpCells.Add(first2);
}
}
return tmpCells;
}
}
public class VerbProperties_ShootSprayMulti : VerbProperties
{
public int numCellsToHit = 1;
}
}

View File

@@ -0,0 +1,76 @@
using RimWorld.Planet;
using UnityEngine;
using Verse;
using RimWorld;
namespace ArachnaeSwarm
{
public class WorldObject_CatastropheMissile : WorldObject
{
public int destinationTile = -1;
public IntVec3 destinationCell = IntVec3.Invalid;
public ThingDef Projectile;
private int initialTile = -1;
private float traveledPct;
private const float TravelSpeed = 0.0002f;
public override void ExposeData()
{
base.ExposeData();
Scribe_Values.Look(ref destinationTile, "destinationTile", 0);
Scribe_Values.Look(ref destinationCell, "destinationCell");
Scribe_Defs.Look(ref Projectile, "Projectile");
Scribe_Values.Look(ref initialTile, "initialTile", 0);
Scribe_Values.Look(ref traveledPct, "traveledPct", 0f);
}
public override void PostAdd()
{
base.PostAdd();
this.initialTile = this.Tile;
}
private Vector3 StartPos => Find.WorldGrid.GetTileCenter(this.initialTile);
private Vector3 EndPos => Find.WorldGrid.GetTileCenter(this.destinationTile);
public override Vector3 DrawPos => Vector3.Slerp(StartPos, EndPos, traveledPct);
protected override void Tick()
{
base.Tick();
float distance = GenMath.SphericalDistance(StartPos.normalized, EndPos.normalized);
if(distance > 0)
{
traveledPct += TravelSpeed / distance;
}
else
{
traveledPct = 1;
}
if (traveledPct >= 1f)
{
Arrived();
}
}
private void Arrived()
{
Map targetMap = Current.Game.FindMap(this.destinationTile);
if (targetMap != null)
{
// Find a random entry point at the north edge of the target map
IntVec3 entryCell = CellFinder.RandomEdgeCell(Rot4.North, targetMap);
// Spawn the final projectile (the cruise missile) at the entry point
Projectile_CruiseMissile missile = (Projectile_CruiseMissile)GenSpawn.Spawn(this.Projectile, entryCell, targetMap, WipeMode.Vanish);
// Launch it from the entry point towards the final destination cell
missile.Launch(null, entryCell.ToVector3Shifted(), new LocalTargetInfo(this.destinationCell), new LocalTargetInfo(this.destinationCell), ProjectileHitFlags.IntendedTarget);
}
Find.WorldObjects.Remove(this);
}
}
}