我去忘记提交了
This commit is contained in:
@@ -0,0 +1,924 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.AI;
|
||||
using Verse.AI.Group;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class ModExt_HasSpeedTurret : DefModExtension
|
||||
{
|
||||
public float speed = 1f;
|
||||
}
|
||||
/// <summary>
|
||||
/// 非瞬时瞄准的炮塔建筑类
|
||||
/// 继承自原版炮塔,增加了平滑旋转瞄准功能
|
||||
/// </summary>
|
||||
public class Building_TurretGunHasSpeed : Building_TurretGun
|
||||
{
|
||||
// 当前炮塔角度
|
||||
public float curAngle;
|
||||
|
||||
/// <summary>
|
||||
/// 旋转速度属性
|
||||
/// 从Mod扩展配置中获取旋转速度,如果没有配置则使用默认值1f
|
||||
/// </summary>
|
||||
public float rotateSpeed
|
||||
{
|
||||
get
|
||||
{
|
||||
ModExt_HasSpeedTurret ext = this.ext;
|
||||
return ext.speed;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Mod扩展配置属性
|
||||
/// 获取炮塔定义的Mod扩展配置
|
||||
/// </summary>
|
||||
public ModExt_HasSpeedTurret ext
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.def.GetModExtension<ModExt_HasSpeedTurret>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 炮塔方向向量
|
||||
/// 根据当前角度计算炮塔的朝向向量
|
||||
/// </summary>
|
||||
public Vector3 turretOrientation
|
||||
{
|
||||
get
|
||||
{
|
||||
return Vector3.forward.RotatedBy(this.curAngle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目标角度差
|
||||
/// 计算当前炮塔方向与目标方向之间的角度差
|
||||
/// </summary>
|
||||
public float deltaAngle
|
||||
{
|
||||
get
|
||||
{
|
||||
return (this.currentTargetInt == null) ? 0f : Vector3.SignedAngle(this.turretOrientation, (this.currentTargetInt.CenterVector3 - this.DrawPos).Yto0(), Vector3.up);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据保存和加载
|
||||
/// 重写ExposeData以保存和加载当前角度数据
|
||||
/// </summary>
|
||||
public override void ExposeData()
|
||||
{
|
||||
base.ExposeData();
|
||||
Scribe_Values.Look<float>(ref this.curAngle, "curAngle", 0f, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(LocalTargetInfo重载)
|
||||
/// </summary>
|
||||
/// <param name="t">目标信息</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(LocalTargetInfo t)
|
||||
{
|
||||
return this.CanAttackTarget(t.CenterVector3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(Thing重载)
|
||||
/// </summary>
|
||||
/// <param name="t">目标物体</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(Thing t)
|
||||
{
|
||||
return this.CanAttackTarget(t.DrawPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以攻击目标(Vector3重载)
|
||||
/// 判断目标是否在当前炮塔的瞄准范围内
|
||||
/// </summary>
|
||||
/// <param name="t">目标位置</param>
|
||||
/// <returns>是否可以攻击</returns>
|
||||
private bool CanAttackTarget(Vector3 t)
|
||||
{
|
||||
return Vector3.Angle(this.turretOrientation, (t - this.DrawPos).Yto0()) <= this.rotateSpeed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每帧更新
|
||||
/// 处理炮塔的旋转逻辑
|
||||
/// </summary>
|
||||
protected override void Tick()
|
||||
{
|
||||
// 如果炮塔处于激活状态且有目标
|
||||
if (base.Active && this.currentTargetInt != null)
|
||||
{
|
||||
// 如果准备开火但角度差过大,延迟开火
|
||||
if (this.burstWarmupTicksLeft == 1 && Mathf.Abs(this.deltaAngle) > this.rotateSpeed)
|
||||
{
|
||||
this.burstWarmupTicksLeft++;
|
||||
}
|
||||
|
||||
// 根据角度差更新当前角度
|
||||
this.curAngle += ((Mathf.Abs(this.deltaAngle) - this.rotateSpeed > 0f) ?
|
||||
(Mathf.Sign(this.deltaAngle) * this.rotateSpeed) : this.deltaAngle);
|
||||
}
|
||||
|
||||
base.Tick();
|
||||
// 规范化角度值到0-360度范围
|
||||
this.curAngle = this.Trim(this.curAngle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角度规范化
|
||||
/// 将角度值限制在0-360度范围内
|
||||
/// </summary>
|
||||
/// <param name="angle">输入角度</param>
|
||||
/// <returns>规范化后的角度</returns>
|
||||
protected float Trim(float angle)
|
||||
{
|
||||
if (angle > 360f)
|
||||
{
|
||||
angle -= 360f;
|
||||
}
|
||||
if (angle < 0f)
|
||||
{
|
||||
angle += 360f;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制炮塔
|
||||
/// 设置炮塔顶部的旋转角度
|
||||
/// </summary>
|
||||
/// <param name="drawLoc">绘制位置</param>
|
||||
/// <param name="flip">是否翻转</param>
|
||||
protected override void DrawAt(Vector3 drawLoc, bool flip = false)
|
||||
{
|
||||
this.top.CurRotation = this.curAngle;
|
||||
base.DrawAt(drawLoc, flip);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取目标搜索器
|
||||
/// 如果有人操作则返回操作者,否则返回炮塔自身
|
||||
/// </summary>
|
||||
/// <returns>目标搜索器</returns>
|
||||
private IAttackTargetSearcher TargSearcher()
|
||||
{
|
||||
if (this.mannableComp != null && this.mannableComp.MannedNow)
|
||||
{
|
||||
return this.mannableComp.ManningPawn;
|
||||
}
|
||||
else
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查目标是否有效
|
||||
/// 过滤不适合攻击的目标
|
||||
/// </summary>
|
||||
/// <param name="t">目标物体</param>
|
||||
/// <returns>目标是否有效</returns>
|
||||
private bool IsValidTarget(Thing t)
|
||||
{
|
||||
Pawn pawn = t as Pawn;
|
||||
if (pawn != null)
|
||||
{
|
||||
// 玩家派系的炮塔不攻击囚犯
|
||||
if (base.Faction == Faction.OfPlayer && pawn.IsPrisoner)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查弹道是否会被厚屋顶阻挡
|
||||
if (this.AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
RoofDef roofDef = base.Map.roofGrid.RoofAt(t.Position);
|
||||
if (roofDef != null && roofDef.isThickRoof)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 无人操作的机械炮塔不攻击友好机械单位
|
||||
if (this.mannableComp == null)
|
||||
{
|
||||
return !GenAI.MachinesLike(base.Faction, pawn);
|
||||
}
|
||||
|
||||
// 有人操作的炮塔不攻击玩家动物
|
||||
if (pawn.RaceProps.Animal && pawn.Faction == Faction.OfPlayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试寻找新目标
|
||||
/// 重写目标选择逻辑,支持角度限制
|
||||
/// </summary>
|
||||
/// <returns>新的目标信息</returns>
|
||||
public override LocalTargetInfo TryFindNewTarget()
|
||||
{
|
||||
IAttackTargetSearcher attackTargetSearcher = this.TargSearcher();
|
||||
Faction faction = attackTargetSearcher.Thing.Faction;
|
||||
float range = this.AttackVerb.verbProps.range;
|
||||
|
||||
Building t;
|
||||
// 50%概率优先攻击殖民者建筑(如果敌对且使用抛射武器)
|
||||
if (Rand.Value < 0.5f && this.AttackVerb.ProjectileFliesOverhead() &&
|
||||
faction.HostileTo(Faction.OfPlayer) &&
|
||||
base.Map.listerBuildings.allBuildingsColonist.Where(delegate (Building x)
|
||||
{
|
||||
float minRange = this.AttackVerb.verbProps.EffectiveMinRange(x, this);
|
||||
float distanceSquared = (float)x.Position.DistanceToSquared(this.Position);
|
||||
return distanceSquared > minRange * minRange && distanceSquared < range * range;
|
||||
}).TryRandomElement(out t))
|
||||
{
|
||||
return t;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 设置目标扫描标志
|
||||
TargetScanFlags targetScanFlags = TargetScanFlags.NeedThreat | TargetScanFlags.NeedAutoTargetable;
|
||||
|
||||
if (!this.AttackVerb.ProjectileFliesOverhead())
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedLOSToAll;
|
||||
targetScanFlags |= TargetScanFlags.LOSBlockableByGas;
|
||||
}
|
||||
|
||||
if (this.AttackVerb.IsIncendiary_Ranged())
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedNonBurning;
|
||||
}
|
||||
|
||||
if (this.def.building.IsMortar)
|
||||
{
|
||||
targetScanFlags |= TargetScanFlags.NeedNotUnderThickRoof;
|
||||
}
|
||||
|
||||
// 使用角度感知的目标查找器
|
||||
return (Thing)AttackTargetFinderAngle.BestShootTargetFromCurrentPosition(
|
||||
attackTargetSearcher, targetScanFlags, this.turretOrientation,
|
||||
new Predicate<Thing>(this.IsValidTarget), 0f, 9999f);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 攻击目标查找器(角度优化版)
|
||||
/// 提供基于角度优化的攻击目标选择功能
|
||||
/// </summary>
|
||||
public static class AttackTargetFinderAngle
|
||||
{
|
||||
// 友军误伤评分偏移量常量
|
||||
private const float FriendlyFireScoreOffsetPerHumanlikeOrMechanoid = 18f; // 每人类或机械族的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetPerAnimal = 7f; // 每动物的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetPerNonPawn = 10f; // 每非pawn单位的友军误伤分数偏移
|
||||
private const float FriendlyFireScoreOffsetSelf = 40f; // 对自己造成误伤的分数偏移
|
||||
// 临时目标列表,用于缓存计算过程中的目标
|
||||
private static List<IAttackTarget> tmpTargets = new List<IAttackTarget>(128);
|
||||
|
||||
// 可用射击目标及其分数的列表
|
||||
private static List<Pair<IAttackTarget, float>> availableShootingTargets = new List<Pair<IAttackTarget, float>>();
|
||||
|
||||
// 临时存储目标分数的列表
|
||||
private static List<float> tmpTargetScores = new List<float>();
|
||||
|
||||
// 临时存储是否可以向目标射击的列表
|
||||
private static List<bool> tmpCanShootAtTarget = new List<bool>();
|
||||
/// <summary>
|
||||
/// 从当前位置寻找最佳射击目标
|
||||
/// </summary>
|
||||
/// <param name="searcher">搜索者(攻击目标搜索器)</param>
|
||||
/// <param name="flags">目标扫描标志</param>
|
||||
/// <param name="angle">射击角度</param>
|
||||
/// <param name="validator">目标验证器(可选)</param>
|
||||
/// <param name="minDistance">最小距离(默认0)</param>
|
||||
/// <param name="maxDistance">最大距离(默认9999)</param>
|
||||
/// <returns>最佳攻击目标,如果没有则返回null</returns>
|
||||
public static IAttackTarget BestShootTargetFromCurrentPosition(
|
||||
IAttackTargetSearcher searcher,
|
||||
TargetScanFlags flags,
|
||||
Vector3 angle,
|
||||
Predicate<Thing> validator = null,
|
||||
float minDistance = 0f,
|
||||
float maxDistance = 9999f)
|
||||
{
|
||||
// 获取当前有效动词(武器)
|
||||
Verb currentEffectiveVerb = searcher.CurrentEffectiveVerb;
|
||||
|
||||
// 检查是否有攻击动词
|
||||
if (currentEffectiveVerb == null)
|
||||
{
|
||||
Log.Error("BestShootTargetFromCurrentPosition with " + searcher.ToStringSafe<IAttackTargetSearcher>() + " who has no attack verb.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算实际的最小和最大距离,考虑武器的属性
|
||||
float actualMinDistance = Mathf.Max(minDistance, currentEffectiveVerb.verbProps.minRange);
|
||||
float actualMaxDistance = Mathf.Min(maxDistance, currentEffectiveVerb.verbProps.range);
|
||||
|
||||
// 调用主要的目标查找方法
|
||||
return BestAttackTarget(
|
||||
searcher,
|
||||
flags,
|
||||
angle,
|
||||
validator,
|
||||
actualMinDistance,
|
||||
actualMaxDistance,
|
||||
default(IntVec3),
|
||||
float.MaxValue,
|
||||
false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找最佳攻击目标(核心方法)
|
||||
/// </summary>
|
||||
/// <param name="searcher">搜索者</param>
|
||||
/// <param name="flags">目标扫描标志</param>
|
||||
/// <param name="angle">射击角度</param>
|
||||
/// <param name="validator">目标验证器</param>
|
||||
/// <param name="minDist">最小距离</param>
|
||||
/// <param name="maxDist">最大距离</param>
|
||||
/// <param name="locus">搜索中心点</param>
|
||||
/// <param name="maxTravelRadiusFromLocus">从中心点的最大移动半径</param>
|
||||
/// <param name="canTakeTargetsCloserThanEffectiveMinRange">是否可以攻击比有效最小距离更近的目标</param>
|
||||
/// <returns>最佳攻击目标</returns>
|
||||
public static IAttackTarget BestAttackTarget(
|
||||
IAttackTargetSearcher searcher,
|
||||
TargetScanFlags flags,
|
||||
Vector3 angle,
|
||||
Predicate<Thing> validator = null,
|
||||
float minDist = 0f,
|
||||
float maxDist = 9999f,
|
||||
IntVec3 locus = default(IntVec3),
|
||||
float maxTravelRadiusFromLocus = float.MaxValue,
|
||||
bool canTakeTargetsCloserThanEffectiveMinRange = true)
|
||||
{
|
||||
// 获取搜索者的Thing对象和当前有效动词
|
||||
Thing searcherThing = searcher.Thing;
|
||||
Verb verb = searcher.CurrentEffectiveVerb;
|
||||
|
||||
// 验证攻击动词是否存在
|
||||
if (verb == null)
|
||||
{
|
||||
Log.Error("BestAttackTarget with " + searcher.ToStringSafe<IAttackTargetSearcher>() + " who has no attack verb.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 初始化各种标志和参数
|
||||
bool onlyTargetMachines = verb.IsEMP(); // 是否只瞄准机械单位(EMP武器)
|
||||
float minDistSquared = minDist * minDist; // 最小距离的平方(用于距离比较优化)
|
||||
|
||||
// 计算从搜索中心点的最大距离平方
|
||||
float maxLocusDist = maxTravelRadiusFromLocus + verb.verbProps.range;
|
||||
float maxLocusDistSquared = maxLocusDist * maxLocusDist;
|
||||
|
||||
// LOS(视线)验证器,用于检查是否被烟雾阻挡
|
||||
Predicate<IntVec3> losValidator = null;
|
||||
if ((flags & TargetScanFlags.LOSBlockableByGas) > TargetScanFlags.None)
|
||||
{
|
||||
losValidator = (IntVec3 vec3) => !vec3.AnyGas(searcherThing.Map, GasType.BlindSmoke);
|
||||
}
|
||||
|
||||
// 获取潜在目标列表
|
||||
tmpTargets.Clear();
|
||||
tmpTargets.AddRange(searcherThing.Map.attackTargetsCache.GetPotentialTargetsFor(searcher));
|
||||
|
||||
// 移除非战斗人员(根据标志)
|
||||
tmpTargets.RemoveAll(t => ShouldIgnoreNoncombatant(searcherThing, t, flags));
|
||||
|
||||
// 内部验证器函数
|
||||
bool InnerValidator(IAttackTarget target, Predicate<IntVec3> losValidator)
|
||||
{
|
||||
Thing targetThing = target.Thing;
|
||||
if (target == searcher)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (minDistSquared > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!canTakeTargetsCloserThanEffectiveMinRange)
|
||||
{
|
||||
float num3 = verb.verbProps.EffectiveMinRange(targetThing, searcherThing);
|
||||
if (num3 > 0f && (float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < num3 * num3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxTravelRadiusFromLocus < 9999f && (float)(targetThing.Position - locus).LengthHorizontalSquared > maxLocusDistSquared)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!searcherThing.HostileTo(targetThing))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (validator != null && !validator(targetThing))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if ((flags & TargetScanFlags.NeedNotUnderThickRoof) != 0)
|
||||
{
|
||||
RoofDef roof = targetThing.Position.GetRoof(targetThing.Map);
|
||||
if (roof != null && roof.isThickRoof)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedLOSToAll) != 0)
|
||||
{
|
||||
if (losValidator != null && (!losValidator(searcherThing.Position) || !losValidator(targetThing.Position)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!searcherThing.CanSee(targetThing))
|
||||
{
|
||||
if (target is Pawn)
|
||||
{
|
||||
if ((flags & TargetScanFlags.NeedLOSToPawns) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ((flags & TargetScanFlags.NeedLOSToNonPawns) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (((flags & TargetScanFlags.NeedThreat) != 0 || (flags & TargetScanFlags.NeedAutoTargetable) != 0) && target.ThreatDisabled(searcher))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedAutoTargetable) != 0 && !AttackTargetFinder.IsAutoTargetable(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedActiveThreat) != 0 && !GenHostility.IsActiveThreatTo(target, searcher.Thing.Faction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Pawn pawn = target as Pawn;
|
||||
if (onlyTargetMachines && pawn != null && pawn.RaceProps.IsFlesh)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((flags & TargetScanFlags.NeedNonBurning) != 0 && targetThing.IsBurning())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searcherThing.def.race != null && (int)searcherThing.def.race.intelligence >= 2)
|
||||
{
|
||||
CompExplosive compExplosive = targetThing.TryGetComp<CompExplosive>();
|
||||
if (compExplosive != null && compExplosive.wickStarted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 距离验证
|
||||
if (!targetThing.Position.InHorDistOf(searcherThing.Position, maxDist))
|
||||
return false;
|
||||
|
||||
// 最小距离验证
|
||||
if (!canTakeTargetsCloserThanEffectiveMinRange &&
|
||||
(float)(searcherThing.Position - targetThing.Position).LengthHorizontalSquared < minDistSquared)
|
||||
return false;
|
||||
|
||||
// 中心点距离验证
|
||||
if (locus.IsValid &&
|
||||
(float)(locus - targetThing.Position).LengthHorizontalSquared > maxLocusDistSquared)
|
||||
return false;
|
||||
|
||||
// 自定义验证器
|
||||
if (validator != null && !validator(targetThing))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否有可以直接射击的目标
|
||||
bool hasDirectShootTarget = false;
|
||||
for (int i = 0; i < tmpTargets.Count; i++)
|
||||
{
|
||||
IAttackTarget attackTarget = tmpTargets[i];
|
||||
if (attackTarget.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) &&
|
||||
InnerValidator(attackTarget, losValidator) &&
|
||||
CanShootAtFromCurrentPosition(attackTarget, searcher, verb))
|
||||
{
|
||||
hasDirectShootTarget = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IAttackTarget bestTarget;
|
||||
|
||||
if (hasDirectShootTarget)
|
||||
{
|
||||
// 如果有可以直接射击的目标,使用基于分数的随机选择
|
||||
tmpTargets.RemoveAll(x => !x.Thing.Position.InHorDistOf(searcherThing.Position, maxDist) || !InnerValidator(x, losValidator));
|
||||
bestTarget = GetRandomShootingTargetByScore(tmpTargets, searcher, verb, angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 否则使用最近的目标选择策略
|
||||
bool needReachableIfCantHit = (flags & TargetScanFlags.NeedReachableIfCantHitFromMyPos) > TargetScanFlags.None;
|
||||
bool needReachable = (flags & TargetScanFlags.NeedReachable) > TargetScanFlags.None;
|
||||
|
||||
Predicate<Thing> reachableValidator;
|
||||
if (!needReachableIfCantHit || needReachable)
|
||||
{
|
||||
reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator);
|
||||
}
|
||||
else
|
||||
{
|
||||
reachableValidator = (Thing t) => InnerValidator((IAttackTarget)t, losValidator) &&
|
||||
CanShootAtFromCurrentPosition((IAttackTarget)t, searcher, verb);
|
||||
}
|
||||
|
||||
bestTarget = (IAttackTarget)GenClosest.ClosestThing_Global(
|
||||
searcherThing.Position,
|
||||
tmpTargets,
|
||||
maxDist,
|
||||
reachableValidator,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
tmpTargets.Clear();
|
||||
return bestTarget;
|
||||
}
|
||||
/// <summary>
|
||||
/// 检查是否应该忽略非战斗人员
|
||||
/// </summary>
|
||||
private static bool ShouldIgnoreNoncombatant(Thing searcherThing, IAttackTarget target, TargetScanFlags flags)
|
||||
{
|
||||
// 只对Pawn类型的目标进行判断
|
||||
if (!(target is Pawn pawn))
|
||||
return false;
|
||||
|
||||
// 如果是战斗人员,不忽略
|
||||
if (pawn.IsCombatant())
|
||||
return false;
|
||||
|
||||
// 如果设置了忽略非战斗人员标志,则忽略
|
||||
if ((flags & TargetScanFlags.IgnoreNonCombatants) > TargetScanFlags.None)
|
||||
return true;
|
||||
|
||||
// 如果看不到非战斗人员,则忽略
|
||||
return !GenSight.LineOfSightToThing(searcherThing.Position, pawn, searcherThing.Map, false, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否可以从当前位置射击目标
|
||||
/// </summary>
|
||||
private static bool CanShootAtFromCurrentPosition(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
return verb != null && verb.CanHitTargetFrom(searcher.Thing.Position, target.Thing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过权重随机获取射击目标
|
||||
/// </summary>
|
||||
private static IAttackTarget GetRandomShootingTargetByScore(List<IAttackTarget> targets, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
|
||||
{
|
||||
var availableTargets = GetAvailableShootingTargetsByScore(targets, searcher, verb, angle);
|
||||
if (availableTargets.TryRandomElementByWeight(x => x.Second, out Pair<IAttackTarget, float> result))
|
||||
{
|
||||
return result.First;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用射击目标及其分数的列表
|
||||
/// </summary>
|
||||
private static List<Pair<IAttackTarget, float>> GetAvailableShootingTargetsByScore(
|
||||
List<IAttackTarget> rawTargets,
|
||||
IAttackTargetSearcher searcher,
|
||||
Verb verb,
|
||||
Vector3 angle)
|
||||
{
|
||||
availableShootingTargets.Clear();
|
||||
|
||||
if (rawTargets.Count == 0)
|
||||
return availableShootingTargets;
|
||||
|
||||
// 初始化临时列表
|
||||
tmpTargetScores.Clear();
|
||||
tmpCanShootAtTarget.Clear();
|
||||
|
||||
float highestScore = float.MinValue;
|
||||
IAttackTarget bestTarget = null;
|
||||
|
||||
// 第一轮遍历:计算基础分数并标记可射击目标
|
||||
for (int i = 0; i < rawTargets.Count; i++)
|
||||
{
|
||||
tmpTargetScores.Add(float.MinValue);
|
||||
tmpCanShootAtTarget.Add(false);
|
||||
|
||||
// 跳过搜索者自身
|
||||
if (rawTargets[i] == searcher)
|
||||
continue;
|
||||
|
||||
// 检查是否可以射击
|
||||
bool canShoot = CanShootAtFromCurrentPosition(rawTargets[i], searcher, verb);
|
||||
tmpCanShootAtTarget[i] = canShoot;
|
||||
|
||||
if (canShoot)
|
||||
{
|
||||
// 计算射击目标分数
|
||||
float score = GetShootingTargetScore(rawTargets[i], searcher, verb, angle);
|
||||
tmpTargetScores[i] = score;
|
||||
|
||||
// 更新最佳目标
|
||||
if (bestTarget == null || score > highestScore)
|
||||
{
|
||||
bestTarget = rawTargets[i];
|
||||
highestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建可用目标列表
|
||||
for (int j = 0; j < rawTargets.Count; j++)
|
||||
{
|
||||
if (rawTargets[j] != searcher && tmpCanShootAtTarget[j])
|
||||
{
|
||||
availableShootingTargets.Add(new Pair<IAttackTarget, float>(rawTargets[j], tmpTargetScores[j]));
|
||||
}
|
||||
}
|
||||
|
||||
return availableShootingTargets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算射击目标分数(核心评分算法)
|
||||
/// </summary>
|
||||
private static float GetShootingTargetScore(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb, Vector3 angle)
|
||||
{
|
||||
float score = 60f; // 基础分数
|
||||
|
||||
// 距离因素:越近分数越高(最多40分)
|
||||
float distance = (target.Thing.Position - searcher.Thing.Position).LengthHorizontal;
|
||||
score -= Mathf.Min(distance, 40f);
|
||||
|
||||
// 目标正在瞄准自己:加分
|
||||
if (target.TargetCurrentlyAimingAt == searcher.Thing)
|
||||
score += 10f;
|
||||
|
||||
// 最近攻击目标:加分(如果最近攻击过这个目标)
|
||||
if (searcher.LastAttackedTarget == target.Thing && Find.TickManager.TicksGame - searcher.LastAttackTargetTick <= 300)
|
||||
score += 40f;
|
||||
|
||||
// 掩体因素:目标有掩体保护则减分
|
||||
float blockChance = CoverUtility.CalculateOverallBlockChance(target.Thing.Position, searcher.Thing.Position, searcher.Thing.Map);
|
||||
score -= blockChance * 10f;
|
||||
|
||||
// Pawn特定因素
|
||||
if (target is Pawn pawnTarget)
|
||||
{
|
||||
// 非战斗人员减分
|
||||
score -= NonCombatantScore(pawnTarget);
|
||||
|
||||
// 远程攻击目标特殊处理
|
||||
if (verb.verbProps.ai_TargetHasRangedAttackScoreOffset != 0f &&
|
||||
pawnTarget.CurrentEffectiveVerb != null &&
|
||||
pawnTarget.CurrentEffectiveVerb.verbProps.Ranged)
|
||||
{
|
||||
score += verb.verbProps.ai_TargetHasRangedAttackScoreOffset;
|
||||
}
|
||||
|
||||
// 倒地目标大幅减分
|
||||
if (pawnTarget.Downed)
|
||||
score -= 50f;
|
||||
}
|
||||
|
||||
// 友军误伤因素
|
||||
score += FriendlyFireBlastRadiusTargetScoreOffset(target, searcher, verb);
|
||||
score += FriendlyFireConeTargetScoreOffset(target, searcher, verb);
|
||||
|
||||
// 角度因素:计算与理想角度的偏差
|
||||
Vector3 targetDirection = (target.Thing.DrawPos - searcher.Thing.DrawPos).Yto0();
|
||||
float angleDeviation = Vector3.Angle(angle, targetDirection);
|
||||
|
||||
// 防止除零错误
|
||||
if (angleDeviation < 0.1f)
|
||||
angleDeviation = 0.1f;
|
||||
|
||||
// 最终分数计算:考虑目标优先级因子和角度偏差
|
||||
float finalScore = score * target.TargetPriorityFactor / angleDeviation;
|
||||
|
||||
// 确保返回正数
|
||||
return Mathf.Max(finalScore, 0.01f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算非战斗人员分数
|
||||
/// </summary>
|
||||
private static float NonCombatantScore(Thing target)
|
||||
{
|
||||
if (!(target is Pawn pawn))
|
||||
return 0f;
|
||||
|
||||
if (!pawn.IsCombatant())
|
||||
return 50f; // 非战斗人员大幅减分
|
||||
|
||||
if (pawn.DevelopmentalStage.Juvenile())
|
||||
return 25f; // 未成年人中等减分
|
||||
|
||||
return 0f; // 战斗成年人不减分
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算爆炸半径内的友军误伤分数偏移
|
||||
/// </summary>
|
||||
private static float FriendlyFireBlastRadiusTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
// 检查是否启用了避免友军误伤半径
|
||||
if (verb.verbProps.ai_AvoidFriendlyFireRadius <= 0f)
|
||||
return 0f;
|
||||
|
||||
Map map = target.Thing.Map;
|
||||
IntVec3 targetPosition = target.Thing.Position;
|
||||
int cellCount = GenRadial.NumCellsInRadius(verb.verbProps.ai_AvoidFriendlyFireRadius);
|
||||
float friendlyFireScore = 0f;
|
||||
|
||||
// 遍历爆炸半径内的所有单元格
|
||||
for (int i = 0; i < cellCount; i++)
|
||||
{
|
||||
IntVec3 checkCell = targetPosition + GenRadial.RadialPattern[i];
|
||||
|
||||
if (!checkCell.InBounds(map))
|
||||
continue;
|
||||
|
||||
bool hasLineOfSight = true;
|
||||
List<Thing> thingsInCell = checkCell.GetThingList(map);
|
||||
|
||||
// 检查单元格内的所有物体
|
||||
for (int j = 0; j < thingsInCell.Count; j++)
|
||||
{
|
||||
Thing thing = thingsInCell[j];
|
||||
|
||||
// 只关心攻击目标且不是当前目标
|
||||
if (!(thing is IAttackTarget) || thing == target)
|
||||
continue;
|
||||
|
||||
// 检查视线(只检查一次)
|
||||
if (hasLineOfSight)
|
||||
{
|
||||
if (!GenSight.LineOfSight(targetPosition, checkCell, map, true, null, 0, 0))
|
||||
break; // 没有视线,跳过这个单元格
|
||||
|
||||
hasLineOfSight = false;
|
||||
}
|
||||
|
||||
// 计算误伤分数
|
||||
float hitScore;
|
||||
if (thing == searcher)
|
||||
hitScore = FriendlyFireScoreOffsetSelf; // 击中自己
|
||||
else if (!(thing is Pawn))
|
||||
hitScore = FriendlyFireScoreOffsetPerNonPawn; // 非Pawn物体
|
||||
else if (thing.def.race.Animal)
|
||||
hitScore = FriendlyFireScoreOffsetPerAnimal; // 动物
|
||||
else
|
||||
hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid; // 人类或机械族
|
||||
|
||||
// 根据敌对关系调整分数
|
||||
if (!searcher.Thing.HostileTo(thing))
|
||||
friendlyFireScore -= hitScore; // 友军:减分
|
||||
else
|
||||
friendlyFireScore += hitScore * 0.6f; // 敌军:小幅加分
|
||||
}
|
||||
}
|
||||
|
||||
return friendlyFireScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算锥形范围内的友军误伤分数偏移
|
||||
/// </summary>
|
||||
private static float FriendlyFireConeTargetScoreOffset(IAttackTarget target, IAttackTargetSearcher searcher, Verb verb)
|
||||
{
|
||||
// 只对Pawn类型的搜索者进行计算
|
||||
if (!(searcher.Thing is Pawn searcherPawn))
|
||||
return 0f;
|
||||
|
||||
// 检查智能等级
|
||||
if (searcherPawn.RaceProps.intelligence < Intelligence.ToolUser)
|
||||
return 0f;
|
||||
|
||||
// 机械族不计算锥形误伤
|
||||
if (searcherPawn.RaceProps.IsMechanoid)
|
||||
return 0f;
|
||||
|
||||
// 只处理射击类动词
|
||||
if (!(verb is Verb_Shoot shootVerb))
|
||||
return 0f;
|
||||
|
||||
ThingDef projectileDef = shootVerb.verbProps.defaultProjectile;
|
||||
if (projectileDef == null)
|
||||
return 0f;
|
||||
|
||||
// 高空飞行的抛射物不计算锥形误伤
|
||||
if (projectileDef.projectile.flyOverhead)
|
||||
return 0f;
|
||||
|
||||
Map map = searcherPawn.Map;
|
||||
|
||||
// 获取射击报告
|
||||
ShotReport report = ShotReport.HitReportFor(searcherPawn, verb, (Thing)target);
|
||||
|
||||
// 计算强制失误半径
|
||||
float forcedMissRadius = Mathf.Max(
|
||||
VerbUtility.CalculateAdjustedForcedMiss(verb.verbProps.ForcedMissRadius, report.ShootLine.Dest - report.ShootLine.Source),
|
||||
1.5f);
|
||||
|
||||
// 获取可能被误伤的所有单元格
|
||||
IEnumerable<IntVec3> potentialHitCells =
|
||||
from dest in GenRadial.RadialCellsAround(report.ShootLine.Dest, forcedMissRadius, true)
|
||||
where dest.InBounds(map)
|
||||
select new ShootLine(report.ShootLine.Source, dest)
|
||||
into line
|
||||
from pos in line.Points().Concat(line.Dest).TakeWhile(pos => pos.CanBeSeenOverFast(map))
|
||||
select pos;
|
||||
|
||||
potentialHitCells = potentialHitCells.Distinct();
|
||||
|
||||
float coneFriendlyFireScore = 0f;
|
||||
|
||||
// 计算锥形范围内的误伤分数
|
||||
foreach (IntVec3 cell in potentialHitCells)
|
||||
{
|
||||
float interceptChance = VerbUtility.InterceptChanceFactorFromDistance(report.ShootLine.Source.ToVector3Shifted(), cell);
|
||||
|
||||
if (interceptChance <= 0f)
|
||||
continue;
|
||||
|
||||
List<Thing> thingsInCell = cell.GetThingList(map);
|
||||
|
||||
for (int i = 0; i < thingsInCell.Count; i++)
|
||||
{
|
||||
Thing thing = thingsInCell[i];
|
||||
|
||||
if (!(thing is IAttackTarget) || thing == target)
|
||||
continue;
|
||||
|
||||
// 计算误伤分数
|
||||
float hitScore;
|
||||
if (thing == searcher)
|
||||
hitScore = FriendlyFireScoreOffsetSelf;
|
||||
else if (!(thing is Pawn))
|
||||
hitScore = FriendlyFireScoreOffsetPerNonPawn;
|
||||
else if (thing.def.race.Animal)
|
||||
hitScore = FriendlyFireScoreOffsetPerAnimal;
|
||||
else
|
||||
hitScore = FriendlyFireScoreOffsetPerHumanlikeOrMechanoid;
|
||||
|
||||
// 根据拦截概率和敌对关系调整分数
|
||||
hitScore *= interceptChance;
|
||||
if (!searcher.Thing.HostileTo(thing))
|
||||
hitScore = -hitScore; // 友军:减分
|
||||
else
|
||||
hitScore *= 0.6f; // 敌军:小幅加分
|
||||
|
||||
coneFriendlyFireScore += hitScore;
|
||||
}
|
||||
}
|
||||
|
||||
return coneFriendlyFireScore;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ namespace WulaFallenEmpire
|
||||
public ThingDef skyfallerDef;
|
||||
public bool destroyBuilding = true;
|
||||
public int delayTicks = 0;
|
||||
public string requiredFlyOverType = "default"; // 需要的 FlyOver 类型
|
||||
// 删除 requiredFlyOverType 字段
|
||||
public bool allowThinRoof = true; // 允许砸穿薄屋顶
|
||||
public bool allowThickRoof = false; // 是否允许在厚岩顶下空投
|
||||
public string requiredFlyOverLabel = "FlyOver"; // 显示给玩家的标签
|
||||
// 删除 requiredFlyOverLabel 字段
|
||||
|
||||
public CompProperties_SkyfallerCaller()
|
||||
{
|
||||
@@ -33,49 +33,63 @@ namespace WulaFallenEmpire
|
||||
|
||||
public bool CanCall => !used && !calling;
|
||||
|
||||
// 获取所需的 FlyOver 显示标签
|
||||
public string RequiredFlyOverLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
// 优先使用建筑配置的显示标签
|
||||
if (!Props.requiredFlyOverLabel.NullOrEmpty())
|
||||
return Props.requiredFlyOverLabel;
|
||||
|
||||
// 如果没有配置,回退到类型名称
|
||||
return Props.requiredFlyOverType;
|
||||
}
|
||||
}
|
||||
// 固定的显示标签
|
||||
public string RequiredFlyOverLabel => "建筑空投飞行器";
|
||||
|
||||
// 检查是否有对应类型的 FlyOver
|
||||
// 检查是否有拥有 BuildingdropperFacility 设施的 FlyOver
|
||||
public bool HasRequiredFlyOver
|
||||
{
|
||||
get
|
||||
{
|
||||
if (parent?.Map == null) return false;
|
||||
|
||||
// 查找地图上所有具有 FlyOverType 组件的物体
|
||||
List<Thing> allThings = parent.Map.listerThings.AllThings;
|
||||
int flyOverCount = 0;
|
||||
int matchingTypeCount = 0;
|
||||
|
||||
foreach (Thing thing in allThings)
|
||||
try
|
||||
{
|
||||
var typeComp = thing.TryGetComp<CompFlyOverType>();
|
||||
if (typeComp != null)
|
||||
// 检查所有FlyOver类型的物体
|
||||
var allFlyOvers = new List<Thing>();
|
||||
var dynamicObjects = parent.Map.dynamicDrawManager.DrawThings;
|
||||
foreach (var thing in dynamicObjects)
|
||||
{
|
||||
flyOverCount++;
|
||||
if (typeComp.FlyOverType == Props.requiredFlyOverType && typeComp.IsRequiredForDrop)
|
||||
if (thing is FlyOver)
|
||||
{
|
||||
matchingTypeCount++;
|
||||
Log.Message($"[SkyfallerCaller] Found required FlyOver of type: {Props.requiredFlyOverType} at {thing.Position}");
|
||||
allFlyOvers.Add(thing);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"[SkyfallerCaller] Found {allFlyOvers.Count} FlyOvers on map");
|
||||
|
||||
foreach (var thing in allFlyOvers)
|
||||
{
|
||||
if (thing is FlyOver flyOver && !flyOver.Destroyed)
|
||||
{
|
||||
// 检查设施
|
||||
var facilitiesComp = flyOver.GetComp<CompFlyOverFacilities>();
|
||||
if (facilitiesComp == null)
|
||||
{
|
||||
Log.Warning($"[SkyfallerCaller] FlyOver at {flyOver.Position} has no CompFlyOverFacilities");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (facilitiesComp.HasFacility("BuildingdropperFacility"))
|
||||
{
|
||||
Log.Message($"[SkyfallerCaller] Found valid FlyOver at {flyOver.Position} with BuildingdropperFacility");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Message($"[SkyfallerCaller] FlyOver at {flyOver.Position} missing BuildingdropperFacility. Has: {string.Join(", ", facilitiesComp.GetActiveFacilities())}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message("[SkyfallerCaller] No FlyOver with BuildingdropperFacility found");
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[SkyfallerCaller] Error in HasRequiredFlyOver: {ex}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Message($"[SkyfallerCaller] Searched {allThings.Count} things, found {flyOverCount} FlyOvers, {matchingTypeCount} matching type: {Props.requiredFlyOverType}");
|
||||
|
||||
return matchingTypeCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +135,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (!HasRequiredFlyOver)
|
||||
{
|
||||
Log.Message($"[SkyfallerCaller] Cannot call: missing required FlyOver type: {Props.requiredFlyOverType}");
|
||||
Log.Message($"[SkyfallerCaller] Cannot call: missing required FlyOver with BuildingdropperFacility");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -161,7 +175,7 @@ namespace WulaFallenEmpire
|
||||
// 显示相应的错误消息
|
||||
if (!HasRequiredFlyOver)
|
||||
{
|
||||
Messages.Message("WULA_NoRequiredFlyOver".Translate(RequiredFlyOverLabel), parent, MessageTypeDefOf.RejectInput);
|
||||
Messages.Message("WULA_NoBuildingDropperFlyOver".Translate(), parent, MessageTypeDefOf.RejectInput);
|
||||
}
|
||||
else if (!CheckRoofConditions)
|
||||
{
|
||||
@@ -272,7 +286,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
if (!HasRequiredFlyOver)
|
||||
{
|
||||
desc += $"\n{"WULA_RequiresFlyOver".Translate(RequiredFlyOverLabel)}";
|
||||
desc += $"\n{"WULA_RequiresBuildingDropperFlyOver".Translate()}";
|
||||
}
|
||||
|
||||
// 添加 null 检查
|
||||
@@ -299,7 +313,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
if (!HasRequiredFlyOver)
|
||||
{
|
||||
return "WULA_NoRequiredFlyOver".Translate(RequiredFlyOverLabel);
|
||||
return "WULA_NoBuildingDropperFlyOver".Translate();
|
||||
}
|
||||
|
||||
if (!CheckRoofConditions)
|
||||
@@ -343,7 +357,7 @@ namespace WulaFallenEmpire
|
||||
// 添加条件信息
|
||||
if (!HasRequiredFlyOver)
|
||||
{
|
||||
status += $"\n{"WULA_MissingFlyOver".Translate(RequiredFlyOverLabel)}";
|
||||
status += $"\n{"WULA_MissingBuildingDropperFlyOver".Translate()}";
|
||||
}
|
||||
|
||||
// 添加 null 检查
|
||||
|
||||
@@ -10,13 +10,13 @@ namespace WulaFallenEmpire
|
||||
public ThingDef aircraftDef; // 对应的战机定义
|
||||
public int aircraftCount = 1; // 起飞后提供的战机数量
|
||||
public ThingDef skyfallerLeaving; // 起飞时的天空坠落者效果
|
||||
|
||||
|
||||
// 新增:自动发射配置
|
||||
public bool autoLaunchEnabled = false; // 是否启用自动发射
|
||||
public int autoLaunchDelayTicks = 600; // 自动发射延迟(ticks,默认10秒)
|
||||
public bool autoLaunchOnConstruction = true; // 建造完成后自动发射
|
||||
public bool autoLaunchOnPowerOn = false; // 通电时自动发射
|
||||
|
||||
|
||||
public CompProperties_AircraftHangar()
|
||||
{
|
||||
compClass = typeof(CompAircraftHangar);
|
||||
@@ -26,16 +26,16 @@ namespace WulaFallenEmpire
|
||||
public class CompAircraftHangar : ThingComp
|
||||
{
|
||||
public CompProperties_AircraftHangar Props => (CompProperties_AircraftHangar)props;
|
||||
|
||||
|
||||
// 新增:自动发射状态
|
||||
private bool autoLaunchScheduled = false;
|
||||
private int autoLaunchTick = -1;
|
||||
private bool hasLaunched = false;
|
||||
|
||||
|
||||
public override void PostSpawnSetup(bool respawningAfterLoad)
|
||||
{
|
||||
base.PostSpawnSetup(respawningAfterLoad);
|
||||
|
||||
|
||||
if (!respawningAfterLoad && Props.autoLaunchEnabled && Props.autoLaunchOnConstruction)
|
||||
{
|
||||
ScheduleAutoLaunch();
|
||||
@@ -53,7 +53,7 @@ namespace WulaFallenEmpire
|
||||
public override void CompTick()
|
||||
{
|
||||
base.CompTick();
|
||||
|
||||
|
||||
// 处理自动发射
|
||||
if (Props.autoLaunchEnabled && !hasLaunched)
|
||||
{
|
||||
@@ -94,7 +94,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
autoLaunchScheduled = true;
|
||||
autoLaunchTick = Find.TickManager.TicksGame + Props.autoLaunchDelayTicks;
|
||||
|
||||
|
||||
Messages.Message("AircraftAutoLaunchScheduled".Translate(Props.aircraftDef.LabelCap, (Props.autoLaunchDelayTicks / 60f).ToString("F1")), parent, MessageTypeDefOf.NeutralEvent);
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
// 获取全局战机管理器
|
||||
WorldComponent_AircraftManager aircraftManager = Find.World.GetComponent<WorldComponent_AircraftManager>();
|
||||
|
||||
|
||||
if (aircraftManager == null)
|
||||
{
|
||||
Log.Error("AircraftManagerNotFound".Translate());
|
||||
@@ -169,10 +169,10 @@ namespace WulaFallenEmpire
|
||||
|
||||
// 立即向全局管理器注册战机
|
||||
aircraftManager.AddAircraft(Props.aircraftDef, Props.aircraftCount, parent.Faction);
|
||||
|
||||
|
||||
// 显示消息
|
||||
Messages.Message("AircraftLaunched".Translate(Props.aircraftCount, Props.aircraftDef.LabelCap), parent, MessageTypeDefOf.PositiveEvent);
|
||||
|
||||
|
||||
// 创建起飞效果(仅视觉效果)
|
||||
if (Props.skyfallerLeaving != null)
|
||||
{
|
||||
@@ -195,25 +195,25 @@ namespace WulaFallenEmpire
|
||||
// 创建 1 单位 Chemfuel 作为 Skyfaller 的内容物
|
||||
Thing chemfuel = ThingMaker.MakeThing(ThingDefOf.Chemfuel);
|
||||
chemfuel.stackCount = 1;
|
||||
|
||||
|
||||
// 创建包含 Chemfuel 的 Skyfaller
|
||||
Skyfaller skyfaller = SkyfallerMaker.MakeSkyfaller(Props.skyfallerLeaving, chemfuel);
|
||||
|
||||
|
||||
// 设置起飞位置(建筑当前位置)
|
||||
IntVec3 takeoffPos = parent.Position;
|
||||
|
||||
|
||||
// 检查地图是否有效
|
||||
if (parent.Map == null)
|
||||
{
|
||||
Log.Error("TakeoffEffectMapNull".Translate());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 生成 Skyfaller
|
||||
GenSpawn.Spawn(skyfaller, takeoffPos, parent.Map);
|
||||
|
||||
|
||||
Log.Message("TakeoffSkyfallerCreated".Translate(takeoffPos));
|
||||
|
||||
|
||||
// 销毁原建筑
|
||||
parent.Destroy(DestroyMode.Vanish);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_FlyOverType : CompProperties
|
||||
{
|
||||
public int laneLevel = 0; // 航道等级
|
||||
public string flyOverType = "default"; // FlyOver 类型标识符
|
||||
public bool isRequiredForDrop = true; // 是否是需要用于空投的类型
|
||||
|
||||
public CompProperties_FlyOverType()
|
||||
{
|
||||
compClass = typeof(CompFlyOverType);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompFlyOverType : ThingComp
|
||||
{
|
||||
private CompProperties_FlyOverType Props => (CompProperties_FlyOverType)props;
|
||||
|
||||
public int LaneLevel => Props.laneLevel;
|
||||
public string FlyOverType => Props.flyOverType;
|
||||
public bool IsRequiredForDrop => Props.isRequiredForDrop;
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
}
|
||||
|
||||
public override string CompInspectStringExtra()
|
||||
{
|
||||
return $"FlyOver Type: {FlyOverType}, Lane Level: {LaneLevel}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,100 +14,10 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
if (!base.CanApplyOn(target, dest))
|
||||
return false;
|
||||
// 检查航道等级系统
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
string disableReason;
|
||||
if (!CanSpawnWithLaneLevelCheck(out disableReason))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 新增:航道等级检查逻辑
|
||||
private bool CanSpawnWithLaneLevelCheck(out string disableReason)
|
||||
{
|
||||
disableReason = null;
|
||||
Map map = parent.pawn.Map;
|
||||
if (map == null)
|
||||
return true;
|
||||
// 获取地图上所有的 FlyOver 物体
|
||||
var existingFlyOvers = map.listerThings.ThingsOfDef(Props.flyOverDef)
|
||||
.OfType<FlyOver>()
|
||||
.Where(f => f.Spawned && !f.Destroyed)
|
||||
.ToList();
|
||||
// 获取当前技能的航道等级和类型
|
||||
int currentLaneLevel = Props.laneLevel;
|
||||
string currentFlyOverType = Props.flyOverTypeName;
|
||||
Log.Message($"Lane Level Check: Current level={currentLaneLevel}, type={currentFlyOverType}, existing flyovers={existingFlyOvers.Count}");
|
||||
// 规则1:航道等级0可以不受限制召唤
|
||||
if (currentLaneLevel == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 规则2:检查是否有更高航道等级的flyover存在
|
||||
var higherLevelFlyOvers = existingFlyOvers
|
||||
.Where(f => GetFlyOverLaneLevel(f) > currentLaneLevel)
|
||||
.ToList();
|
||||
if (higherLevelFlyOvers.Any())
|
||||
{
|
||||
disableReason = "Cannot summon: Higher lane level flyover exists";
|
||||
Log.Message($"Cannot summon: Found {higherLevelFlyOvers.Count} higher level flyovers");
|
||||
return false;
|
||||
}
|
||||
// 规则3:检查是否有相同航道等级和类型的flyover存在
|
||||
var sameTypeAndLevelFlyOvers = existingFlyOvers
|
||||
.Where(f => GetFlyOverLaneLevel(f) == currentLaneLevel &&
|
||||
GetFlyOverType(f) == currentFlyOverType)
|
||||
.ToList();
|
||||
if (sameTypeAndLevelFlyOvers.Any())
|
||||
{
|
||||
disableReason = $"Cannot summon: Same lane level and type flyover already exists";
|
||||
Log.Message($"Cannot summon: Found {sameTypeAndLevelFlyOvers.Count} same type and level flyovers");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 新增:获取FlyOver的航道等级
|
||||
private int GetFlyOverLaneLevel(FlyOver flyOver)
|
||||
{
|
||||
var comp = flyOver.GetComp<CompFlyOverType>();
|
||||
return comp?.LaneLevel ?? 0;
|
||||
}
|
||||
// 新增:获取FlyOver的类型
|
||||
private string GetFlyOverType(FlyOver flyOver)
|
||||
{
|
||||
var comp = flyOver.GetComp<CompFlyOverType>();
|
||||
return comp?.FlyOverType ?? "default";
|
||||
}
|
||||
// 新增:强制销毁低等级FlyOver
|
||||
private void DestroyLowerLevelFlyOvers()
|
||||
{
|
||||
Map map = parent.pawn.Map;
|
||||
if (map == null) return;
|
||||
var existingFlyOvers = map.listerThings.ThingsOfDef(Props.flyOverDef)
|
||||
.OfType<FlyOver>()
|
||||
.Where(f => f.Spawned && !f.Destroyed)
|
||||
.ToList();
|
||||
int currentLaneLevel = Props.laneLevel;
|
||||
string currentFlyOverType = Props.flyOverTypeName;
|
||||
// 查找需要销毁的低等级FlyOver
|
||||
var flyOversToDestroy = existingFlyOvers
|
||||
.Where(f => GetFlyOverLaneLevel(f) < currentLaneLevel ||
|
||||
(GetFlyOverLaneLevel(f) == currentLaneLevel &&
|
||||
GetFlyOverType(f) != currentFlyOverType))
|
||||
.ToList();
|
||||
foreach (var flyOver in flyOversToDestroy)
|
||||
{
|
||||
Log.Message($"Destroying lower level flyover: Level={GetFlyOverLaneLevel(flyOver)}, Type={GetFlyOverType(flyOver)}");
|
||||
flyOver.EmergencyDestroy();
|
||||
}
|
||||
if (flyOversToDestroy.Count > 0)
|
||||
{
|
||||
Log.Message($"Destroyed {flyOversToDestroy.Count} lower level flyovers");
|
||||
}
|
||||
}
|
||||
// 在 CompAbilityEffect_SpawnFlyOver.cs 中修改航道检查方法:
|
||||
// 修改 DestroyLowerLevelFlyOvers 方法(现在由全局管理器处理):
|
||||
public override void Apply(LocalTargetInfo target, LocalTargetInfo dest)
|
||||
{
|
||||
base.Apply(target, dest);
|
||||
@@ -115,18 +25,6 @@ namespace WulaFallenEmpire
|
||||
return;
|
||||
try
|
||||
{
|
||||
// 检查航道等级系统
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
string disableReason;
|
||||
if (!CanSpawnWithLaneLevelCheck(out disableReason))
|
||||
{
|
||||
Messages.Message(disableReason, MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
// 强制销毁低等级FlyOver
|
||||
DestroyLowerLevelFlyOvers();
|
||||
}
|
||||
Log.Message($"FlyOver skill activated by {parent.pawn.Label} at position {parent.pawn.Position}");
|
||||
Log.Message($"Target cell: {target.Cell}, Dest: {dest.Cell}");
|
||||
// 计算起始和结束位置
|
||||
@@ -183,19 +81,6 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
baseInfo = $"扇形监视: 约{Props.strafeWidth * 2 + 1}格宽度\n(具体参数在飞行物定义中)";
|
||||
}
|
||||
// 添加航道等级信息
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(baseInfo))
|
||||
baseInfo += "\n";
|
||||
baseInfo += $"航道等级: {Props.laneLevel}";
|
||||
// 检查是否可以被召唤
|
||||
string disableReason;
|
||||
if (!CanSpawnWithLaneLevelCheck(out disableReason))
|
||||
{
|
||||
baseInfo += $"\n<color=red>{disableReason}</color>";
|
||||
}
|
||||
}
|
||||
return baseInfo;
|
||||
}
|
||||
// 更新:验证方法,包含航道等级检查
|
||||
@@ -207,19 +92,6 @@ namespace WulaFallenEmpire
|
||||
return false;
|
||||
if (!target.Cell.IsValid || !target.Cell.InBounds(parent.pawn.Map))
|
||||
return false;
|
||||
// 航道等级检查
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
string disableReason;
|
||||
if (!CanSpawnWithLaneLevelCheck(out disableReason))
|
||||
{
|
||||
if (throwMessages)
|
||||
{
|
||||
Messages.Message(disableReason, MessageTypeDefOf.RejectInput);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// 更新:创建标准飞越方法,添加航道等级组件
|
||||
@@ -241,19 +113,6 @@ namespace WulaFallenEmpire
|
||||
);
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
// 设置航道等级信息
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
var laneComp = flyOver.GetComp<CompFlyOverType>();
|
||||
if (laneComp != null)
|
||||
{
|
||||
Log.Message($"FlyOver created with lane level: {Props.laneLevel}, type: {Props.flyOverTypeName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("FlyOver def does not have CompFlyOverType component!");
|
||||
}
|
||||
}
|
||||
if (Props.customSound != null)
|
||||
{
|
||||
// 自定义音效逻辑
|
||||
@@ -281,15 +140,6 @@ namespace WulaFallenEmpire
|
||||
// 设置基本属性
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
// 设置航道等级信息
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
var laneComp = flyOver.GetComp<CompFlyOverType>();
|
||||
if (laneComp != null)
|
||||
{
|
||||
Log.Message($"Ground Strafing FlyOver created with lane level: {Props.laneLevel}, type: {Props.flyOverTypeName}");
|
||||
}
|
||||
}
|
||||
// 获取扫射组件并设置预处理后的目标单元格
|
||||
CompGroundStrafing strafingComp = flyOver.GetComp<CompGroundStrafing>();
|
||||
if (strafingComp != null)
|
||||
@@ -344,15 +194,6 @@ namespace WulaFallenEmpire
|
||||
// 设置基本属性
|
||||
flyOver.spawnContentsOnImpact = Props.dropContentsOnImpact;
|
||||
flyOver.playFlyOverSound = Props.playFlyOverSound;
|
||||
// 设置航道等级信息
|
||||
if (Props.useLaneLevelSystem)
|
||||
{
|
||||
var laneComp = flyOver.GetComp<CompFlyOverType>();
|
||||
if (laneComp != null)
|
||||
{
|
||||
Log.Message($"Sector Surveillance FlyOver created with lane level: {Props.laneLevel}, type: {Props.flyOverTypeName}");
|
||||
}
|
||||
}
|
||||
Log.Message($"SectorSurveillance FlyOver created: {flyOver} from {startPos} to {endPos}");
|
||||
}
|
||||
// 新增:验证和优化飞行路径
|
||||
|
||||
@@ -47,11 +47,6 @@ namespace WulaFallenEmpire
|
||||
public bool showSectorPreview = true; // 是否显示扇形预览
|
||||
public Color sectorPreviewColor = new Color(0.3f, 0.7f, 1f, 0.3f);
|
||||
|
||||
// 航道等级系统配置
|
||||
public bool useLaneLevelSystem = false; // 是否使用航道等级系统
|
||||
public int laneLevel = 0; // 该技能的航道等级
|
||||
public string flyOverTypeName = "default"; // FlyOver类型名称
|
||||
|
||||
public CompProperties_AbilitySpawnFlyOver()
|
||||
{
|
||||
this.compClass = typeof(CompAbilityEffect_SpawnFlyOver);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 在 Building_GlobalWorkTable.cs 中添加材质处理逻辑
|
||||
// Building_GlobalWorkTable.cs (修改版本)
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -73,7 +73,7 @@ namespace WulaFallenEmpire
|
||||
public GlobalWorkTableAirdropExtension AirdropExtension =>
|
||||
def.GetModExtension<GlobalWorkTableAirdropExtension>();
|
||||
|
||||
// 新增:添加空投命令到技能栏
|
||||
// 修改:添加空投命令到技能栏,添加工厂设施检查
|
||||
public override IEnumerable<Gizmo> GetGizmos()
|
||||
{
|
||||
foreach (Gizmo g in base.GetGizmos())
|
||||
@@ -81,9 +81,12 @@ namespace WulaFallenEmpire
|
||||
yield return g;
|
||||
}
|
||||
|
||||
// 只有在有输出物品时才显示空投按钮
|
||||
// 只有在有输出物品且有工厂设施的飞行器时才显示空投按钮
|
||||
var globalStorage = Find.World.GetComponent<GlobalStorageWorldComponent>();
|
||||
if (globalStorage != null && globalStorage.outputStorage.Any(kvp => kvp.Value > 0))
|
||||
bool hasOutputItems = globalStorage != null && globalStorage.outputStorage.Any(kvp => kvp.Value > 0);
|
||||
bool hasFactoryFlyOver = HasFactoryFacilityFlyOver();
|
||||
|
||||
if (hasOutputItems && hasFactoryFlyOver)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
@@ -91,9 +94,73 @@ namespace WulaFallenEmpire
|
||||
defaultLabel = "WULA_AirdropProducts".Translate(),
|
||||
defaultDesc = "WULA_AirdropProductsDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_AirdropProducts"),
|
||||
disabledReason = "WULA_CannotAirdrop".Translate()
|
||||
};
|
||||
}
|
||||
else if (hasOutputItems && !hasFactoryFlyOver)
|
||||
{
|
||||
yield return new Command_Action
|
||||
{
|
||||
action = () => Messages.Message("WULA_NoFactoryFlyOver".Translate(), MessageTypeDefOf.RejectInput),
|
||||
defaultLabel = "WULA_AirdropProducts".Translate(),
|
||||
defaultDesc = "WULA_NoFactoryFlyOverDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_AirdropProducts"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:检查是否有拥有FactoryFacility设施的飞行器
|
||||
private bool HasFactoryFacilityFlyOver()
|
||||
{
|
||||
Map map = Map;
|
||||
if (map == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
// 检查所有FlyOver类型的物体
|
||||
var allFlyOvers = new List<Thing>();
|
||||
var dynamicObjects = map.dynamicDrawManager.DrawThings;
|
||||
foreach (var thing in dynamicObjects)
|
||||
{
|
||||
if (thing is FlyOver)
|
||||
{
|
||||
allFlyOvers.Add(thing);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message($"[FactoryFacility Check] Found {allFlyOvers.Count} FlyOvers on map");
|
||||
|
||||
foreach (var thing in allFlyOvers)
|
||||
{
|
||||
if (thing is FlyOver flyOver && !flyOver.Destroyed)
|
||||
{
|
||||
// 检查设施
|
||||
var facilitiesComp = flyOver.GetComp<CompFlyOverFacilities>();
|
||||
if (facilitiesComp == null)
|
||||
{
|
||||
Log.Warning($"[FactoryFacility Check] FlyOver at {flyOver.Position} has no CompFlyOverFacilities");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (facilitiesComp.HasFacility("FactoryFacility"))
|
||||
{
|
||||
Log.Message($"[FactoryFacility Check] Found valid FlyOver at {flyOver.Position} with FactoryFacility");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Message($"[FactoryFacility Check] FlyOver at {flyOver.Position} missing FactoryFacility. Has: {string.Join(", ", facilitiesComp.GetActiveFacilities())}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.Message("[FactoryFacility Check] No FlyOver with FactoryFacility found");
|
||||
return false;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[FactoryFacility Check] Error in HasFactoryFacilityFlyOver: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:开始空投目标选择
|
||||
@@ -106,6 +173,14 @@ namespace WulaFallenEmpire
|
||||
Messages.Message("WULA_NoProductsToAirdrop".Translate(), MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有工厂设施的飞行器
|
||||
if (!HasFactoryFacilityFlyOver())
|
||||
{
|
||||
Messages.Message("WULA_NoFactoryFlyOver".Translate(), MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动目标选择
|
||||
Find.Targeter.BeginTargeting(new TargetingParameters
|
||||
{
|
||||
@@ -133,6 +208,13 @@ namespace WulaFallenEmpire
|
||||
if (globalStorage == null)
|
||||
return;
|
||||
|
||||
// 再次检查是否有工厂设施的飞行器
|
||||
if (!HasFactoryFacilityFlyOver())
|
||||
{
|
||||
Messages.Message("WULA_NoFactoryFlyOver".Translate(), MessageTypeDefOf.RejectInput);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取空投参数
|
||||
var airdropExt = AirdropExtension;
|
||||
float maxRange = airdropExt?.maxRange ?? 50f;
|
||||
|
||||
@@ -4,6 +4,7 @@ using UnityEngine;
|
||||
using Verse;
|
||||
using Verse.Sound;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
@@ -11,6 +12,9 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
public new CompProperties_Launchable_ToGlobalStorage Props => (CompProperties_Launchable_ToGlobalStorage)this.props;
|
||||
|
||||
// 获取垃圾屏蔽组件
|
||||
public CompGarbageShield GarbageShieldComp => this.parent.GetComp<CompGarbageShield>();
|
||||
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
// 移除原有的发射按钮,替换为我们自己的
|
||||
@@ -59,6 +63,30 @@ namespace WulaFallenEmpire
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查垃圾屏蔽 - 如果启用了垃圾屏蔽并且有禁止物品,取消发射
|
||||
if (GarbageShieldComp != null && GarbageShieldComp.GarbageShieldEnabled)
|
||||
{
|
||||
List<Thing> forbiddenItems = GarbageShieldComp.GetForbiddenItems(transporter.innerContainer);
|
||||
if (forbiddenItems.Count > 0)
|
||||
{
|
||||
// 显示取消发射消息
|
||||
StringBuilder forbiddenList = new StringBuilder();
|
||||
foreach (Thing item in forbiddenItems)
|
||||
{
|
||||
if (forbiddenList.Length > 0) forbiddenList.Append(", ");
|
||||
forbiddenList.Append($"{item.LabelCap} x{item.stackCount}");
|
||||
}
|
||||
|
||||
Messages.Message("WULA_LaunchCancelledDueToForbiddenItems".Translate(forbiddenList.ToString()),
|
||||
this.parent, MessageTypeDefOf.RejectInput);
|
||||
|
||||
// 触发垃圾屏蔽UI事件
|
||||
GarbageShieldComp.ProcessGarbageShieldTrigger(forbiddenItems);
|
||||
|
||||
return; // 取消发射
|
||||
}
|
||||
}
|
||||
|
||||
// 统计发送的物品
|
||||
int inputItemsCount = 0;
|
||||
int outputItemsCount = 0;
|
||||
@@ -87,7 +115,8 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
|
||||
// 2. 显示发送结果消息
|
||||
string message = BuildTransferMessage(inputItemsCount, outputItemsCount, inputItemsList.ToString(), outputItemsList.ToString());
|
||||
string message = BuildTransferMessage(inputItemsCount, outputItemsCount,
|
||||
inputItemsList.ToString(), outputItemsList.ToString());
|
||||
Messages.Message(message, this.parent, MessageTypeDefOf.PositiveEvent);
|
||||
|
||||
// 3. 清空容器,防止物品掉落
|
||||
@@ -108,20 +137,13 @@ namespace WulaFallenEmpire
|
||||
if (item.def.IsApparel)
|
||||
return true;
|
||||
|
||||
// 活着的Pawn
|
||||
//if (item is Pawn pawn && !pawn.Dead)
|
||||
// return true;
|
||||
|
||||
// Pawn的尸体
|
||||
if (item.def.IsCorpse)
|
||||
return true;
|
||||
|
||||
// 其他物品发送到输入存储器
|
||||
return false;
|
||||
}
|
||||
|
||||
// 构建转移消息
|
||||
private string BuildTransferMessage(int inputCount, int outputCount, string inputList, string outputList)
|
||||
private string BuildTransferMessage(int inputCount, int outputCount,
|
||||
string inputList, string outputList)
|
||||
{
|
||||
StringBuilder message = new StringBuilder();
|
||||
|
||||
@@ -156,6 +178,11 @@ namespace WulaFallenEmpire
|
||||
message.Append(": ").Append(outputList);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有任何物品
|
||||
message.Append("WULA_NoItemsProcessed".Translate());
|
||||
}
|
||||
|
||||
return message.ToString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using RimWorld;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public class CompProperties_GarbageShield : CompProperties
|
||||
{
|
||||
public bool garbageShieldEnabled = false; // 通过XML配置启用/禁用
|
||||
public string garbageShieldUIEventDefName; // 垃圾屏蔽触发时弹出的UI事件defName
|
||||
|
||||
public CompProperties_GarbageShield()
|
||||
{
|
||||
this.compClass = typeof(CompGarbageShield);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompGarbageShield : ThingComp
|
||||
{
|
||||
public CompProperties_GarbageShield Props => (CompProperties_GarbageShield)this.props;
|
||||
|
||||
// 垃圾屏蔽状态完全由XML配置决定,不提供玩家切换
|
||||
public bool GarbageShieldEnabled => Props.garbageShieldEnabled;
|
||||
|
||||
// 检查物品是否是被禁止的垃圾物品
|
||||
public bool IsForbiddenItem(Thing thing)
|
||||
{
|
||||
if (!GarbageShieldEnabled) return false;
|
||||
|
||||
// 检查是否是殖民者
|
||||
if (thing is Pawn pawn && pawn.IsColonist)
|
||||
return true;
|
||||
|
||||
// 检查是否是尸体
|
||||
if (thing.def.IsCorpse)
|
||||
return true;
|
||||
|
||||
// 检查是否是有毒垃圾
|
||||
if (IsToxicWaste(thing))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取所有禁止物品
|
||||
public List<Thing> GetForbiddenItems(ThingOwner container)
|
||||
{
|
||||
List<Thing> forbiddenItems = new List<Thing>();
|
||||
|
||||
if (!GarbageShieldEnabled) return forbiddenItems;
|
||||
|
||||
foreach (Thing item in container)
|
||||
{
|
||||
if (IsForbiddenItem(item))
|
||||
{
|
||||
forbiddenItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return forbiddenItems;
|
||||
}
|
||||
|
||||
// 判断是否为有毒垃圾
|
||||
private bool IsToxicWaste(Thing thing)
|
||||
{
|
||||
// 根据物品标签、类别或定义名称判断是否为有毒垃圾
|
||||
return thing.def == ThingDefOf.Wastepack;
|
||||
}
|
||||
|
||||
// 处理垃圾屏蔽触发并触发UI事件
|
||||
public void ProcessGarbageShieldTrigger(List<Thing> forbiddenItems)
|
||||
{
|
||||
if (forbiddenItems.Count > 0 && !string.IsNullOrEmpty(Props.garbageShieldUIEventDefName))
|
||||
{
|
||||
// 弹出指定的自定义UI
|
||||
EventDef uiDef = DefDatabase<EventDef>.GetNamed(Props.garbageShieldUIEventDefName, false);
|
||||
if (uiDef != null)
|
||||
{
|
||||
Find.WindowStack.Add(new Dialog_CustomDisplay(uiDef));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[CompGarbageShield] Could not find EventDef named '{Props.garbageShieldUIEventDefName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,14 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
public float fuelNeededToLaunch = 25f;
|
||||
public SoundDef launchSound;
|
||||
|
||||
// 垃圾屏蔽配置 - 通过XML控制
|
||||
public bool garbageShieldEnabled = false;
|
||||
public string garbageShieldUIEventDefName = "Wula_UI_Legion_Reply_1";
|
||||
|
||||
public CompProperties_Launchable_ToGlobalStorage()
|
||||
{
|
||||
this.compClass = typeof(CompLaunchable_ToGlobalStorage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// CompProperties_ProductionCategory.cs
|
||||
using RimWorld;
|
||||
using Verse;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
public enum ProductionCategory
|
||||
{
|
||||
Equipment, // 装备
|
||||
Weapon, // 武器
|
||||
Mechanoid // 机械体
|
||||
}
|
||||
|
||||
public class CompProperties_ProductionCategory : CompProperties
|
||||
{
|
||||
public ProductionCategory category = ProductionCategory.Equipment;
|
||||
|
||||
public CompProperties_ProductionCategory()
|
||||
{
|
||||
this.compClass = typeof(CompProductionCategory);
|
||||
}
|
||||
}
|
||||
|
||||
public class CompProductionCategory : ThingComp
|
||||
{
|
||||
public CompProperties_ProductionCategory Props => (CompProperties_ProductionCategory)props;
|
||||
|
||||
public ProductionCategory Category => Props.category;
|
||||
}
|
||||
}
|
||||
@@ -14,86 +14,271 @@ namespace WulaFallenEmpire
|
||||
private float viewHeight = 1000f;
|
||||
private Vector2 scrollPosition;
|
||||
private GlobalProductionOrder mouseoverOrder;
|
||||
|
||||
|
||||
private static readonly Vector2 WinSize = new Vector2(420f, 480f);
|
||||
|
||||
// 分类按钮状态
|
||||
private ProductionCategory? selectedCategory = null;
|
||||
|
||||
protected Building_GlobalWorkTable SelTable => (Building_GlobalWorkTable)base.SelThing;
|
||||
|
||||
|
||||
public ITab_GlobalBills()
|
||||
{
|
||||
size = WinSize;
|
||||
labelKey = "WULA_GlobalBillsTab";
|
||||
tutorTag = "GlobalBills";
|
||||
}
|
||||
|
||||
protected override void FillTab()
|
||||
{
|
||||
Rect mainRect = new Rect(0f, 0f, WinSize.x, WinSize.y).ContractedBy(10f);
|
||||
|
||||
|
||||
// 标题
|
||||
Text.Font = GameFont.Medium;
|
||||
Widgets.Label(new Rect(mainRect.x, mainRect.y, mainRect.width, 30f), "WULA_GlobalProduction".Translate());
|
||||
Text.Font = GameFont.Small;
|
||||
|
||||
// 存储查看按钮 - 放在标题旁边
|
||||
|
||||
// 存储查看按钮
|
||||
Rect storageButtonRect = new Rect(mainRect.xMax - 160f, mainRect.y, 120f, 25f);
|
||||
DoStorageButton(storageButtonRect);
|
||||
|
||||
|
||||
// 分类按钮区域
|
||||
Rect categoryButtonsRect = new Rect(mainRect.x, mainRect.y + 35f, mainRect.width, 25f);
|
||||
DoCategoryButtons(categoryButtonsRect);
|
||||
|
||||
// 上帝模式按钮区域
|
||||
if (DebugSettings.godMode)
|
||||
{
|
||||
Rect godModeButtonRect = new Rect(mainRect.x, mainRect.y + 35f, mainRect.width, 25f);
|
||||
Rect godModeButtonRect = new Rect(mainRect.x, mainRect.y + 65f, mainRect.width, 25f);
|
||||
DoGodModeButtons(godModeButtonRect);
|
||||
}
|
||||
|
||||
// 订单列表区域 - 调整位置
|
||||
float ordersRectY = DebugSettings.godMode ? mainRect.y + 65f : mainRect.y + 35f;
|
||||
Rect ordersRect = new Rect(mainRect.x, ordersRectY, mainRect.width, mainRect.height - (DebugSettings.godMode ? 110f : 80f));
|
||||
|
||||
// 订单列表区域
|
||||
float ordersRectY = DebugSettings.godMode ? mainRect.y + 90f : mainRect.y + 65f;
|
||||
Rect ordersRect = new Rect(mainRect.x, ordersRectY, mainRect.width, mainRect.height - (DebugSettings.godMode ? 130f : 100f));
|
||||
mouseoverOrder = DoOrdersListing(ordersRect);
|
||||
|
||||
// 添加订单按钮
|
||||
|
||||
// 添加订单按钮(现在显示选中的分类)
|
||||
Rect addButtonRect = new Rect(mainRect.x, mainRect.yMax - 35f, mainRect.width, 30f);
|
||||
if (Widgets.ButtonText(addButtonRect, "WULA_AddProductionOrder".Translate()))
|
||||
DoAddOrderButton(addButtonRect);
|
||||
}
|
||||
// 新增:分类按钮
|
||||
private void DoCategoryButtons(Rect rect)
|
||||
{
|
||||
float buttonWidth = (rect.width - 10f) / 3f;
|
||||
|
||||
Rect equipmentRect = new Rect(rect.x, rect.y, buttonWidth, rect.height);
|
||||
Rect weaponRect = new Rect(rect.x + buttonWidth + 5f, rect.y, buttonWidth, rect.height);
|
||||
Rect mechanoidRect = new Rect(rect.x + (buttonWidth + 5f) * 2, rect.y, buttonWidth, rect.height);
|
||||
|
||||
// 装备按钮
|
||||
string equipmentLabel = selectedCategory == ProductionCategory.Equipment ?
|
||||
$"<color=yellow>{"WULA_Equipment".Translate()}</color>" :
|
||||
"WULA_Equipment".Translate();
|
||||
|
||||
if (Widgets.ButtonText(equipmentRect, equipmentLabel))
|
||||
{
|
||||
Find.WindowStack.Add(new FloatMenu(GenerateRecipeOptions()));
|
||||
selectedCategory = selectedCategory == ProductionCategory.Equipment ? null : ProductionCategory.Equipment;
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
}
|
||||
|
||||
// 武器按钮
|
||||
string weaponLabel = selectedCategory == ProductionCategory.Weapon ?
|
||||
$"<color=yellow>{"WULA_Weapon".Translate()}</color>" :
|
||||
"WULA_Weapon".Translate();
|
||||
|
||||
if (Widgets.ButtonText(weaponRect, weaponLabel))
|
||||
{
|
||||
selectedCategory = selectedCategory == ProductionCategory.Weapon ? null : ProductionCategory.Weapon;
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
}
|
||||
|
||||
// 机械体按钮
|
||||
string mechanoidLabel = selectedCategory == ProductionCategory.Mechanoid ?
|
||||
$"<color=yellow>{"WULA_Mechanoid".Translate()}</color>" :
|
||||
"WULA_Mechanoid".Translate();
|
||||
|
||||
if (Widgets.ButtonText(mechanoidRect, mechanoidLabel))
|
||||
{
|
||||
selectedCategory = selectedCategory == ProductionCategory.Mechanoid ? null : ProductionCategory.Mechanoid;
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
}
|
||||
}
|
||||
// 修改:添加订单按钮,显示当前选择的分类
|
||||
private void DoAddOrderButton(Rect rect)
|
||||
{
|
||||
string buttonLabel = selectedCategory.HasValue ?
|
||||
$"{"WULA_AddProductionOrder".Translate()} ({GetCategoryLabel(selectedCategory.Value)})" :
|
||||
"WULA_AddProductionOrder".Translate();
|
||||
|
||||
// 新增:存储查看按钮
|
||||
if (Widgets.ButtonText(rect, buttonLabel))
|
||||
{
|
||||
Find.WindowStack.Add(new FloatMenu(GenerateRecipeOptions()));
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
}
|
||||
|
||||
// 如果没有选择分类,显示提示
|
||||
if (!selectedCategory.HasValue && Mouse.IsOver(rect))
|
||||
{
|
||||
TooltipHandler.TipRegion(rect, "WULA_SelectCategoryFirst".Translate());
|
||||
}
|
||||
}
|
||||
// 获取分类标签
|
||||
private string GetCategoryLabel(ProductionCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
ProductionCategory.Equipment => "WULA_Equipment".Translate(),
|
||||
ProductionCategory.Weapon => "WULA_Weapon".Translate(),
|
||||
ProductionCategory.Mechanoid => "WULA_Mechanoid".Translate(),
|
||||
_ => "WULA_Unknown".Translate()
|
||||
};
|
||||
}
|
||||
// 修改:根据选择的分类生成配方选项
|
||||
private List<FloatMenuOption> GenerateRecipeOptions()
|
||||
{
|
||||
var options = new List<FloatMenuOption>();
|
||||
|
||||
// 如果没有选择分类,显示所有配方
|
||||
if (!selectedCategory.HasValue)
|
||||
{
|
||||
foreach (var recipe in SelTable.def.AllRecipes)
|
||||
{
|
||||
if (recipe.AvailableNow && recipe.AvailableOnNow(SelTable))
|
||||
{
|
||||
options.Add(CreateRecipeOption(recipe));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 根据选择的分类筛选配方
|
||||
foreach (var recipe in SelTable.def.AllRecipes)
|
||||
{
|
||||
if (recipe.AvailableNow && recipe.AvailableOnNow(SelTable) &&
|
||||
RecipeMatchesCategory(recipe, selectedCategory.Value))
|
||||
{
|
||||
options.Add(CreateRecipeOption(recipe));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Count == 0)
|
||||
{
|
||||
options.Add(new FloatMenuOption("WULA_NoAvailableRecipes".Translate(), null));
|
||||
}
|
||||
|
||||
// 按显示优先级排序
|
||||
options.SortByDescending(opt => opt.orderInPriority);
|
||||
|
||||
return options;
|
||||
}
|
||||
// 创建配方选项
|
||||
private FloatMenuOption CreateRecipeOption(RecipeDef recipe)
|
||||
{
|
||||
string label = recipe.LabelCap;
|
||||
|
||||
// 添加分类标签
|
||||
var category = GetRecipeCategory(recipe);
|
||||
if (category.HasValue)
|
||||
{
|
||||
label += $" [{GetCategoryLabel(category.Value)}]";
|
||||
}
|
||||
|
||||
return new FloatMenuOption(
|
||||
label: label,
|
||||
action: () =>
|
||||
{
|
||||
var newOrder = new GlobalProductionOrder
|
||||
{
|
||||
recipe = recipe,
|
||||
targetCount = 1,
|
||||
paused = true
|
||||
};
|
||||
SelTable.globalOrderStack.AddOrder(newOrder);
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
},
|
||||
shownItemForIcon: recipe.UIIconThing,
|
||||
thingStyle: null,
|
||||
forceBasicStyle: false,
|
||||
priority: MenuOptionPriority.Default,
|
||||
mouseoverGuiAction: null,
|
||||
revalidateClickTarget: null,
|
||||
extraPartWidth: 29f,
|
||||
extraPartOnGUI: (Rect rect) =>
|
||||
{
|
||||
return Widgets.InfoCardButton(rect.x + 5f, rect.y + (rect.height - 24f) / 2f, recipe);
|
||||
},
|
||||
revalidateWorldClickTarget: null,
|
||||
playSelectionSound: true,
|
||||
orderInPriority: -recipe.displayPriority
|
||||
);
|
||||
}
|
||||
// 检查配方是否匹配分类
|
||||
private bool RecipeMatchesCategory(RecipeDef recipe, ProductionCategory category)
|
||||
{
|
||||
var recipeCategory = GetRecipeCategory(recipe);
|
||||
return recipeCategory == category;
|
||||
}
|
||||
// 获取配方的分类
|
||||
private ProductionCategory? GetRecipeCategory(RecipeDef recipe)
|
||||
{
|
||||
if (recipe.products == null || recipe.products.Count == 0)
|
||||
return null;
|
||||
|
||||
ThingDef productDef = recipe.products[0].thingDef;
|
||||
if (productDef == null)
|
||||
return null;
|
||||
|
||||
// 检查产品是否有分类组件
|
||||
var categoryComp = productDef.GetCompProperties<CompProperties_ProductionCategory>();
|
||||
if (categoryComp != null)
|
||||
return categoryComp.category;
|
||||
|
||||
// 如果没有分类组件,根据产品类型推断
|
||||
return InferCategoryFromThingDef(productDef);
|
||||
}
|
||||
// 根据ThingDef推断分类
|
||||
private ProductionCategory? InferCategoryFromThingDef(ThingDef thingDef)
|
||||
{
|
||||
if (thingDef.IsMeleeWeapon || thingDef.IsRangedWeapon)
|
||||
return ProductionCategory.Weapon;
|
||||
|
||||
if (thingDef.IsApparel || thingDef.equipmentType != EquipmentType.None)
|
||||
return ProductionCategory.Equipment;
|
||||
|
||||
if (thingDef.race != null && thingDef.race.IsMechanoid)
|
||||
return ProductionCategory.Mechanoid;
|
||||
|
||||
return null;
|
||||
}
|
||||
// 其余方法保持不变(DoStorageButton, DoGodModeButtons, DoOrdersListing, DoOrderRow 等)
|
||||
private void DoStorageButton(Rect rect)
|
||||
{
|
||||
// 绘制按钮
|
||||
if (Widgets.ButtonText(rect, "WULA_ViewStorage".Translate()))
|
||||
{
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
}
|
||||
|
||||
// 鼠标悬停时显示存储信息Tooltip
|
||||
|
||||
if (Mouse.IsOver(rect))
|
||||
{
|
||||
TooltipHandler.TipRegion(rect, GetStorageTooltip());
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:获取存储信息的Tooltip
|
||||
private string GetStorageTooltip()
|
||||
{
|
||||
var globalStorage = Find.World.GetComponent<GlobalStorageWorldComponent>();
|
||||
if (globalStorage == null)
|
||||
return "WULA_NoGlobalStorage".Translate();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 输入存储(原材料)
|
||||
sb.AppendLine("WULA_InputStorage".Translate() + ":");
|
||||
sb.AppendLine();
|
||||
|
||||
var inputItems = globalStorage.inputStorage
|
||||
.Where(kvp => kvp.Value > 0)
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.ThenBy(kvp => kvp.Key.label)
|
||||
.ToList();
|
||||
|
||||
if (inputItems.Count == 0)
|
||||
{
|
||||
sb.AppendLine("WULA_NoItems".Translate());
|
||||
@@ -105,19 +290,15 @@ namespace WulaFallenEmpire
|
||||
sb.AppendLine($" {kvp.Value} {kvp.Key.LabelCap}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// 输出存储(产品)
|
||||
sb.AppendLine("WULA_OutputStorage".Translate() + ":");
|
||||
sb.AppendLine();
|
||||
|
||||
var outputItems = globalStorage.outputStorage
|
||||
.Where(kvp => kvp.Value > 0)
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.ThenBy(kvp => kvp.Key.label)
|
||||
.ToList();
|
||||
|
||||
if (outputItems.Count == 0)
|
||||
{
|
||||
sb.AppendLine("WULA_NoItems".Translate());
|
||||
@@ -129,7 +310,6 @@ namespace WulaFallenEmpire
|
||||
sb.AppendLine($" {kvp.Value} {kvp.Key.LabelCap}");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -138,12 +318,12 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
Rect button1Rect = new Rect(rect.x, rect.y, rect.width / 2 - 5f, rect.height);
|
||||
Rect button2Rect = new Rect(rect.x + rect.width / 2 + 5f, rect.y, rect.width / 2 - 5f, rect.height);
|
||||
|
||||
|
||||
if (Widgets.ButtonText(button1Rect, "GOD: Add Resources"))
|
||||
{
|
||||
AddTestResources();
|
||||
}
|
||||
|
||||
|
||||
if (Widgets.ButtonText(button2Rect, "GOD: Spawn Products"))
|
||||
{
|
||||
SpawnOutputProducts();
|
||||
@@ -158,11 +338,11 @@ namespace WulaFallenEmpire
|
||||
// 添加200钢铁
|
||||
ThingDef steelDef = ThingDefOf.Steel;
|
||||
globalStorage.AddToInputStorage(steelDef, 200);
|
||||
|
||||
|
||||
// 添加100零部件
|
||||
ThingDef componentDef = ThingDefOf.ComponentIndustrial;
|
||||
globalStorage.AddToInputStorage(componentDef, 100);
|
||||
|
||||
|
||||
Messages.Message("Added 200 Steel and 100 Components to global storage", MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
@@ -175,15 +355,15 @@ namespace WulaFallenEmpire
|
||||
Map map = SelTable.Map;
|
||||
IntVec3 spawnCell = SelTable.Position;
|
||||
int totalSpawned = 0;
|
||||
|
||||
|
||||
// 复制列表以避免修改时枚举
|
||||
var outputCopy = new Dictionary<ThingDef, int>(globalStorage.outputStorage);
|
||||
|
||||
|
||||
foreach (var kvp in outputCopy)
|
||||
{
|
||||
ThingDef thingDef = kvp.Key;
|
||||
int count = kvp.Value;
|
||||
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
// 创建物品并放置到地图上
|
||||
@@ -192,7 +372,7 @@ namespace WulaFallenEmpire
|
||||
int stackSize = Mathf.Min(count, thingDef.stackLimit);
|
||||
Thing thing = ThingMaker.MakeThing(thingDef);
|
||||
thing.stackCount = stackSize;
|
||||
|
||||
|
||||
if (GenPlace.TryPlaceThing(thing, spawnCell, map, ThingPlaceMode.Near))
|
||||
{
|
||||
globalStorage.RemoveFromOutputStorage(thingDef, stackSize);
|
||||
@@ -206,7 +386,7 @@ namespace WulaFallenEmpire
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Messages.Message($"Spawned {totalSpawned} items at worktable location", MessageTypeDefOf.PositiveEvent);
|
||||
}
|
||||
}
|
||||
@@ -214,28 +394,28 @@ namespace WulaFallenEmpire
|
||||
private GlobalProductionOrder DoOrdersListing(Rect rect)
|
||||
{
|
||||
GlobalProductionOrder result = null;
|
||||
|
||||
|
||||
Widgets.DrawMenuSection(rect);
|
||||
Rect outRect = rect.ContractedBy(5f);
|
||||
Rect viewRect = new Rect(0f, 0f, outRect.width - 16f, viewHeight);
|
||||
|
||||
|
||||
Widgets.BeginScrollView(outRect, ref scrollPosition, viewRect);
|
||||
|
||||
|
||||
float curY = 0f;
|
||||
for (int i = 0; i < SelTable.globalOrderStack.orders.Count; i++)
|
||||
{
|
||||
var order = SelTable.globalOrderStack.orders[i];
|
||||
|
||||
|
||||
// 增加订单行高度
|
||||
Rect orderRect = new Rect(0f, curY, viewRect.width, 90f);
|
||||
|
||||
|
||||
if (DoOrderRow(orderRect, order))
|
||||
{
|
||||
result = order;
|
||||
}
|
||||
|
||||
|
||||
curY += 95f; // 增加行间距
|
||||
|
||||
|
||||
// 分隔线
|
||||
if (i < SelTable.globalOrderStack.orders.Count - 1)
|
||||
{
|
||||
@@ -243,12 +423,12 @@ namespace WulaFallenEmpire
|
||||
curY += 5f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Event.current.type == EventType.Layout)
|
||||
{
|
||||
viewHeight = curY;
|
||||
}
|
||||
|
||||
|
||||
Widgets.EndScrollView();
|
||||
return result;
|
||||
}
|
||||
@@ -387,53 +567,6 @@ namespace WulaFallenEmpire
|
||||
return Mouse.IsOver(rect);
|
||||
}
|
||||
|
||||
// 简化:在添加订单时移除材质初始化
|
||||
private List<FloatMenuOption> GenerateRecipeOptions()
|
||||
{
|
||||
var options = new List<FloatMenuOption>();
|
||||
foreach (var recipe in SelTable.def.AllRecipes)
|
||||
{
|
||||
if (recipe.AvailableNow && recipe.AvailableOnNow(SelTable))
|
||||
{
|
||||
string label = recipe.LabelCap;
|
||||
options.Add(new FloatMenuOption(
|
||||
label: label,
|
||||
action: () =>
|
||||
{
|
||||
var newOrder = new GlobalProductionOrder
|
||||
{
|
||||
recipe = recipe,
|
||||
targetCount = 1,
|
||||
paused = true
|
||||
};
|
||||
|
||||
SelTable.globalOrderStack.AddOrder(newOrder);
|
||||
SoundDefOf.Click.PlayOneShotOnCamera();
|
||||
},
|
||||
shownItemForIcon: recipe.UIIconThing,
|
||||
thingStyle: null,
|
||||
forceBasicStyle: false,
|
||||
priority: MenuOptionPriority.Default,
|
||||
mouseoverGuiAction: null,
|
||||
revalidateClickTarget: null,
|
||||
extraPartWidth: 29f,
|
||||
extraPartOnGUI: (Rect rect) =>
|
||||
{
|
||||
return Widgets.InfoCardButton(rect.x + 5f, rect.y + (rect.height - 24f) / 2f, recipe);
|
||||
},
|
||||
revalidateWorldClickTarget: null,
|
||||
playSelectionSound: true,
|
||||
orderInPriority: -recipe.displayPriority
|
||||
));
|
||||
}
|
||||
}
|
||||
if (options.Count == 0)
|
||||
{
|
||||
options.Add(new FloatMenuOption("WULA_NoAvailableRecipes".Translate(), null));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
// 新增:立刻完成订单的方法
|
||||
private void CompleteOrderImmediately(GlobalProductionOrder order)
|
||||
{
|
||||
@@ -446,7 +579,7 @@ namespace WulaFallenEmpire
|
||||
|
||||
// 检查是否有足够资源
|
||||
bool hasEnoughResources = order.HasEnoughResources();
|
||||
|
||||
|
||||
if (!hasEnoughResources)
|
||||
{
|
||||
// 上帝模式下,如果没有足够资源,显示确认对话框
|
||||
@@ -478,13 +611,13 @@ namespace WulaFallenEmpire
|
||||
|
||||
// 计算需要完成的数量
|
||||
int remainingCount = order.targetCount - order.currentCount;
|
||||
|
||||
|
||||
if (remainingCount <= 0)
|
||||
return;
|
||||
|
||||
// 尝试消耗资源(如果可能)
|
||||
bool resourcesConsumed = order.ConsumeResources();
|
||||
|
||||
|
||||
if (!resourcesConsumed)
|
||||
{
|
||||
Log.Message($"[GOD MODE] Could not consume resources for {order.recipe.defName}, completing without resource consumption");
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
using RimWorld;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace WulaFallenEmpire
|
||||
{
|
||||
// 自定义属性组件,用于存储半径和颜色信息
|
||||
public class CompProperties_CustomRadius : CompProperties
|
||||
{
|
||||
public float radius = 10f;
|
||||
public Color color = new Color(0.8f, 0.8f, 0.4f); // 默认浅黄色
|
||||
public float radiusOffset = -2.1f; // 半径偏移量,与原始保持一致
|
||||
public bool showInGUI = true; // 是否在GUI中显示切换选项
|
||||
public string label = "Show Radius"; // 直接定义标签文本
|
||||
public string description = "Toggle visibility of the custom radius overlay."; // 直接定义描述文本
|
||||
public bool defaultVisible = true; // 默认是否可见
|
||||
|
||||
public CompProperties_CustomRadius()
|
||||
{
|
||||
this.compClass = typeof(CompCustomRadius);
|
||||
}
|
||||
}
|
||||
|
||||
// 实际的组件类
|
||||
public class CompCustomRadius : ThingComp
|
||||
{
|
||||
private bool radiusVisible = true;
|
||||
|
||||
public CompProperties_CustomRadius Props
|
||||
{
|
||||
get
|
||||
{
|
||||
return (CompProperties_CustomRadius)this.props;
|
||||
}
|
||||
}
|
||||
|
||||
public float EffectiveRadius
|
||||
{
|
||||
get
|
||||
{
|
||||
return Props.radius + Props.radiusOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RadiusVisible
|
||||
{
|
||||
get { return radiusVisible && Props.showInGUI; }
|
||||
set { radiusVisible = value; }
|
||||
}
|
||||
|
||||
public override void Initialize(CompProperties props)
|
||||
{
|
||||
base.Initialize(props);
|
||||
radiusVisible = Props.defaultVisible;
|
||||
}
|
||||
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
Scribe_Values.Look(ref radiusVisible, "radiusVisible", Props.defaultVisible);
|
||||
}
|
||||
|
||||
// 在检视面板中显示切换选项
|
||||
public override IEnumerable<Gizmo> CompGetGizmosExtra()
|
||||
{
|
||||
if (!Props.showInGUI) yield break;
|
||||
|
||||
// 创建切换 gizmo
|
||||
Command_Toggle toggleCommand = new Command_Toggle();
|
||||
toggleCommand.defaultLabel = Props.label;
|
||||
toggleCommand.defaultDesc = Props.description;
|
||||
|
||||
// 尝试加载图标,如果失败则使用默认图标
|
||||
try
|
||||
{
|
||||
toggleCommand.icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_ShowRadius", false);
|
||||
if (toggleCommand.icon == null)
|
||||
{
|
||||
// 使用一个简单的占位符图标
|
||||
toggleCommand.icon = BaseContent.BadTex;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
toggleCommand.icon = BaseContent.BadTex;
|
||||
}
|
||||
|
||||
toggleCommand.isActive = () => RadiusVisible;
|
||||
toggleCommand.toggleAction = () => RadiusVisible = !RadiusVisible;
|
||||
|
||||
yield return toggleCommand;
|
||||
}
|
||||
|
||||
// 获取绘制颜色(考虑透明度等)
|
||||
public Color GetDrawColor()
|
||||
{
|
||||
return Props.color;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义放置工作器
|
||||
public class PlaceWorker_CustomRadius : PlaceWorker
|
||||
{
|
||||
public override void DrawGhost(ThingDef def, IntVec3 center, Rot4 rot, Color ghostCol, Thing thing = null)
|
||||
{
|
||||
// 如果已经有物体存在,则检查其组件的可见性设置
|
||||
if (thing != null)
|
||||
{
|
||||
CompCustomRadius comp = thing.TryGetComp<CompCustomRadius>();
|
||||
if (comp == null || !comp.RadiusVisible)
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取自定义半径组件属性
|
||||
CompProperties_CustomRadius compProperties = def.GetCompProperties<CompProperties_CustomRadius>();
|
||||
if (compProperties != null && compProperties.showInGUI)
|
||||
{
|
||||
float effectiveRadius = compProperties.radius + compProperties.radiusOffset;
|
||||
if (effectiveRadius > 0f)
|
||||
{
|
||||
// 使用指定的颜色绘制圆环
|
||||
Color drawColor = compProperties.color;
|
||||
if (thing != null)
|
||||
{
|
||||
CompCustomRadius comp = thing.TryGetComp<CompCustomRadius>();
|
||||
if (comp != null)
|
||||
{
|
||||
drawColor = comp.GetDrawColor();
|
||||
}
|
||||
}
|
||||
GenDraw.DrawRadiusRing(center, effectiveRadius, drawColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 可选:在验证放置位置时也考虑半径
|
||||
public override AcceptanceReport AllowsPlacing(BuildableDef checkingDef, IntVec3 loc, Rot4 rot, Map map, Thing thingToIgnore = null, Thing thing = null)
|
||||
{
|
||||
// 这里可以添加额外的放置验证逻辑
|
||||
// 例如检查半径内是否有不允许的建筑等
|
||||
|
||||
return true; // 默认允许放置
|
||||
}
|
||||
}
|
||||
|
||||
// 为已放置的建筑添加绘制支持
|
||||
[StaticConstructorOnStartup]
|
||||
public static class CustomRadiusRenderer
|
||||
{
|
||||
static CustomRadiusRenderer()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用Harmony为MapInterface.MapInterfaceUpdate方法添加补丁
|
||||
var harmony = new Harmony("WulaFallenEmpire.CustomRadius");
|
||||
|
||||
// 尝试不同的绘制方法
|
||||
var mapInterfaceMethod = AccessTools.Method(typeof(MapInterface), "MapInterfaceUpdate");
|
||||
if (mapInterfaceMethod != null)
|
||||
{
|
||||
harmony.Patch(mapInterfaceMethod,
|
||||
postfix: new HarmonyMethod(typeof(CustomRadiusRenderer), nameof(Postfix_MapInterfaceUpdate)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("[CustomRadius] Could not find MapInterface.MapInterfaceUpdate method");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[CustomRadius] Error in static constructor: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Postfix_MapInterfaceUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Find.CurrentMap == null) return;
|
||||
|
||||
// 绘制所有带有自定义半径组件的已放置建筑
|
||||
foreach (var thing in Find.CurrentMap.listerThings.AllThings)
|
||||
{
|
||||
if (thing.Spawned && thing.def.HasComp(typeof(CompCustomRadius)))
|
||||
{
|
||||
CompCustomRadius comp = thing.TryGetComp<CompCustomRadius>();
|
||||
if (comp != null && comp.RadiusVisible)
|
||||
{
|
||||
float effectiveRadius = comp.EffectiveRadius;
|
||||
if (effectiveRadius > 0f)
|
||||
{
|
||||
GenDraw.DrawRadiusRing(thing.Position, effectiveRadius, comp.GetDrawColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
// 静默处理错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 为建筑选择时添加绘制支持
|
||||
[StaticConstructorOnStartup]
|
||||
public static class CustomRadiusSelectionRenderer
|
||||
{
|
||||
static CustomRadiusSelectionRenderer()
|
||||
{
|
||||
try
|
||||
{
|
||||
var harmony = new Harmony("WulaFallenEmpire.CustomRadiusSelection");
|
||||
|
||||
// 尝试为选择器绘制方法添加补丁
|
||||
var selectionDrawMethod = AccessTools.Method(typeof(SelectionDrawer), "DrawSelectionOverlays");
|
||||
if (selectionDrawMethod != null)
|
||||
{
|
||||
harmony.Patch(selectionDrawMethod,
|
||||
postfix: new HarmonyMethod(typeof(CustomRadiusSelectionRenderer), nameof(Postfix_DrawSelectionOverlays)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("[CustomRadius] Could not find SelectionDrawer.DrawSelectionOverlays method");
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.Error($"[CustomRadius] Error in static constructor: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Postfix_DrawSelectionOverlays()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Find.Selector == null) return;
|
||||
|
||||
foreach (object selected in Find.Selector.SelectedObjectsListForReading)
|
||||
{
|
||||
if (selected is Thing thing && thing.Spawned && thing.def.HasComp(typeof(CompCustomRadius)))
|
||||
{
|
||||
CompCustomRadius comp = thing.TryGetComp<CompCustomRadius>();
|
||||
if (comp != null && comp.RadiusVisible)
|
||||
{
|
||||
float effectiveRadius = comp.EffectiveRadius;
|
||||
if (effectiveRadius > 0f)
|
||||
{
|
||||
GenDraw.DrawRadiusRing(thing.Position, effectiveRadius, comp.GetDrawColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 静默处理错误
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using HarmonyLib;
|
||||
using HarmonyLib;
|
||||
using RimWorld;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
defaultLabel = "WULA_EjectDataPack".Translate(),
|
||||
defaultDesc = "WULA_EjectDataPackDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/EjectDataPack"),
|
||||
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_EjectDataPack"),
|
||||
action = EjectDataPack
|
||||
};
|
||||
}
|
||||
@@ -108,7 +108,7 @@ namespace WulaFallenEmpire
|
||||
{
|
||||
defaultLabel = "WULA_AbsorbDataPack".Translate(),
|
||||
defaultDesc = "WULA_AbsorbDataPackDesc".Translate(),
|
||||
icon = ContentFinder<Texture2D>.Get("UI/Commands/AbsorbDataPack"),
|
||||
icon = ContentFinder<Texture2D>.Get("Wula/UI/Commands/WULA_AbsorbDataPack"),
|
||||
action = AbsorbDataPack
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Ability\WULA_AbilityBombardment\CompAbilityEffect_Bombardment.cs" />
|
||||
<Compile Include="Ability\WULA_AbilityBombardment\CompProperties_AbilityBombardment.cs" />
|
||||
<Compile Include="BuildingComp\Building_TurretGunHasSpeed.cs" />
|
||||
<Compile Include="BuildingComp\WULA_InitialFaction\CompProperties_InitialFaction.cs" />
|
||||
<Compile Include="BuildingComp\WULA_MechanoidRecycler\Building_MechanoidRecycler.cs" />
|
||||
<Compile Include="BuildingComp\WULA_MechanoidRecycler\CompProperties_MechanoidRecycler.cs" />
|
||||
@@ -101,7 +102,6 @@
|
||||
<Compile Include="Flyover\WULA_FlyOverEscort\CompProperties_FlyOverEscort.cs" />
|
||||
<Compile Include="Flyover\WULA_FlyOverFacilities\CompAbilityEffect_RequireFlyOverFacility.cs" />
|
||||
<Compile Include="Flyover\WULA_FlyOverFacilities\CompFlyOverFacilities.cs" />
|
||||
<Compile Include="Flyover\WULA_FlyOverType\CompProperties_FlyOverType.cs" />
|
||||
<Compile Include="Flyover\WULA_GroundStrafing\CompGroundStrafing.cs" />
|
||||
<Compile Include="Flyover\WULA_SectorSurveillance\CompSectorSurveillance.cs" />
|
||||
<Compile Include="Flyover\WULA_SendLetterAfterTicks\CompProperties_SendLetterAfterTicks.cs" />
|
||||
@@ -111,6 +111,8 @@
|
||||
<Compile Include="Flyover\WULA_SpawnFlyOver\CompAbilityEffect_SpawnFlyOver.cs" />
|
||||
<Compile Include="Flyover\WULA_SpawnFlyOver\CompProperties_AbilitySpawnFlyOver.cs" />
|
||||
<Compile Include="GlobalWorkTable\Building_GlobalWorkTable.cs" />
|
||||
<Compile Include="GlobalWorkTable\CompProperties_GarbageShield.cs" />
|
||||
<Compile Include="GlobalWorkTable\CompProperties_ProductionCategory.cs" />
|
||||
<Compile Include="GlobalWorkTable\GlobalProductionOrder.cs" />
|
||||
<Compile Include="GlobalWorkTable\GlobalProductionOrderStack.cs" />
|
||||
<Compile Include="GlobalWorkTable\GlobalStorageWorldComponent.cs" />
|
||||
@@ -160,6 +162,7 @@
|
||||
<Compile Include="Pawn\WULA_Maintenance\MaintenanceNeedExtension.cs" />
|
||||
<Compile Include="Pawn\WULA_Maintenance\Need_Maintenance.cs" />
|
||||
<Compile Include="Pawn\WULA_Maintenance\WorkGiver_DoMaintenance.cs" />
|
||||
<Compile Include="Placeworker\CompProperties_CustomRadius.cs" />
|
||||
<Compile Include="Stat\StatWorker_Energy.cs" />
|
||||
<Compile Include="Stat\StatWorker_Maintenance.cs" />
|
||||
<Compile Include="Stat\StatWorker_NanoRepair.cs" />
|
||||
|
||||
Reference in New Issue
Block a user