Цинерарий-старатель (#64): дух добывает ископаемые

Урна necroCinerariumProspector (и ...Skull): в «Содержимом» череп и образец - один из девяти ресурсов (камень, глина,
песок, древесина, железо, свинец, уголь, селитра, сланец), нить к ящику. Дух отлетает на соседнюю свободную клетку и
уходит на 5 блоков под землю, растворяясь; раз в игровой час поднимается так же, кладёт добычу в ящик и уходит снова.
За сутки: камень/глина/песок/древесина 30000, руды 18000, ходка ±20%. Из руды - шанс на алмаз, серебро, золото по
шансам игры на каждый «блок» руды. Ящик полон - дух ждёт с добычей. Без зоны и вне лимита урн.

Рецепт - кузня: латунь 40, прах 100, кости 10, кровь/сок, железная кирка; открывается с остальными урнами.
Консоль necroprospector: on/off, trip, yield, spread, depth, состояние. Тексты - 13 языков; глава книги
«Цинерарии» - лист Fig. VIII с шестой урной и фраза про prospektisto.

Проверено в игре (test8): ходка железа 625 в ящике, спуск и подъём духа, лист и страница книги помещаются.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexCube
2026-10-22 11:09:12 +03:00
co-authored by Claude Opus 5.5
parent f1010f98b7
commit 0999fd3afc
12 changed files with 760 additions and 17 deletions
File diff suppressed because one or more lines are too long
+16
View File
@@ -642,5 +642,21 @@
<property name="CreativeMode" value="None"/> <property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumLootDesc"/> <property name="DescriptionKey" value="necroCinerariumLootDesc"/>
</block> </block>
<!-- Урна старателя (22.10.2026, #64; CinerariumProspector.cs): бронзовая катушка с золотой чеканкой на основе банки лутера
(BlenderWork/necroCinerariumProspector, necroCinerariumProspectorPrefab в бандле necrocinerarium). В «Содержимом» череп
и образец - что добывать; дух уходит под землю и раз в игровой час приносит добычу в ящик по нити. Без зоны и вне
лимита, как логист. -->
<block name="necroCinerariumProspector">
<property name="Extends" value="necroCinerarium"/>
<property name="CreativeMode" value="Player"/>
<property name="DescriptionKey" value="necroCinerariumProspectorDesc"/>
<property name="CustomIcon" value="necroCinerariumProspector"/>
<property name="Model" value="#@modfolder(NecromancerTome):Resources/necrocinerarium?necroCinerariumProspectorPrefab.prefab"/>
</block>
<block name="necroCinerariumProspectorSkull">
<property name="Extends" value="necroCinerariumProspector"/>
<property name="CreativeMode" value="None"/>
<property name="DescriptionKey" value="necroCinerariumProspectorDesc"/>
</block>
</append> </append>
</config> </config>
+1 -1
View File
@@ -181,7 +181,7 @@
пользуется, вода сидит на necroNecromancyLvl20. --> пользуется, вода сидит на necroNecromancyLvl20. -->
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/> <unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/> <unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
<unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot" unlock_tier="5"/> <unlock_entry item="necroModKnifeDarkSense,necroCinerarium,necroCinerariumLogist,necroCinerariumWorker,necroCinerariumRepair,necroCinerariumLoot,necroCinerariumProspector" unlock_tier="5"/>
</display_entry> </display_entry>
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из <!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
+30
View File
@@ -731,6 +731,36 @@
<ingredient name="resourceNecroBlackRoseJuice" count="1"/> <ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="resourceLockPick" count="1"/> <ingredient name="resourceLockPick" count="1"/>
</recipe> </recipe>
<!-- Урна старателя (22.10, #64) - бронзовая катушка: в кузне латунь 40 вместо железа и свинца, железная кирка вместо отмычки;
две версии кузни (плавильня / лом), на крови и на соке. Открывается вместе с остальными урнами. -->
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter">
<ingredient name="unit_brass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" material_based="true" tags="learnable,necroNecromancyLvl300,use_smelter">
<ingredient name="unit_brass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter">
<ingredient name="resourceScrapBrass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
<recipe name="necroCinerariumProspector" count="1" craft_area="forge" craft_time="60" tags="learnable,necroNecromancyLvl300,replace_smelter">
<ingredient name="resourceScrapBrass" count="40"/>
<ingredient name="resourceZombieAsh" count="100"/>
<ingredient name="resourceBone" count="10"/>
<ingredient name="resourceNecroBlackRoseJuice" count="1"/>
<ingredient name="meleeToolPickT1IronPickaxe" count="1"/>
</recipe>
</append> </append>
<!-- Магическая пыль в ступке (18.10.2026): повреждённое хранилище - 4, целое - 30. --> <!-- Магическая пыль в ступке (18.10.2026): повреждённое хранилище - 4, целое - 30. -->
<append xpath="/recipes"> <append xpath="/recipes">
+20 -8
View File
@@ -64,7 +64,7 @@ namespace NecromancerTome
/// <summary>Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит, /// <summary>Урна без зоны 5x5 (логист, рабочий, ремонтник, лутер): ставится вплотную к любым урнам, в лимит не входит,
/// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.</summary> /// работает по нитям. У ремонтника и лутера своя большая зона (CinerariumZone.cs) - зоны таких урн могут пересекаться.</summary>
public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b) || IsRepairBlock(_b) || IsLootBlock(_b); public static bool IsZonelessBlock(Block _b) => IsLogistBlock(_b) || IsWorkerBlock(_b) || IsRepairBlock(_b) || IsLootBlock(_b) || IsProspectorBlock(_b);
public const string SkullItemName = "necroSkull"; public const string SkullItemName = "necroSkull";
public const int HalfWidth = 2; public const int HalfWidth = 2;
public const int Below = 1; public const int Below = 1;
@@ -103,7 +103,7 @@ namespace NecromancerTome
} }
} }
bool _worker; bool _worker;
public bool IsZoneless => IsLogist || IsWorker || IsBigZone; public bool IsZoneless => IsLogist || IsWorker || IsBigZone || IsProspector;
/// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary> /// <summary>Урна-логист - по блоку (necroCinerariumLogist / ...Skull); узнаётся при постановке или по миру.</summary>
public bool IsLogist public bool IsLogist
{ {
@@ -119,6 +119,7 @@ namespace NecromancerTome
_worker = IsWorkerBlock(bv.Block); _worker = IsWorkerBlock(bv.Block);
_repair = IsRepairBlock(bv.Block); _repair = IsRepairBlock(bv.Block);
_loot = IsLootBlock(bv.Block); _loot = IsLootBlock(bv.Block);
_prospector = IsProspectorBlock(bv.Block);
_logistKnown = true; _logistKnown = true;
} }
} }
@@ -168,16 +169,17 @@ namespace NecromancerTome
_worker = IsWorkerBlock(_blockValue.Block); _worker = IsWorkerBlock(_blockValue.Block);
_repair = IsRepairBlock(_blockValue.Block); _repair = IsRepairBlock(_blockValue.Block);
_loot = IsLootBlock(_blockValue.Block); _loot = IsLootBlock(_blockValue.Block);
_prospector = IsProspectorBlock(_blockValue.Block);
_logistKnown = true; _logistKnown = true;
string placed = _blockValue.Block?.GetBlockName(); string placed = _blockValue.Block?.GetBlockName();
if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName || if (placed == SkullBlockName || placed == LogistSkullBlockName || placed == LogistWallSkullBlockName || placed == WorkerSkullBlockName ||
placed == RepairSkullBlockName || placed == LootSkullBlockName) placed == RepairSkullBlockName || placed == LootSkullBlockName || placed == ProspectorSkullBlockName)
{ {
HasSkull = true; // поставили урну, поднятую с черепом HasSkull = true; // поставили урну, поднятую с черепом
} }
if (ConnectionManager.Instance.IsServer) if (ConnectionManager.Instance.IsServer)
{ {
CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker || _repair || _loot); CinerariumRegistry.Add(_blockPos, Parent?.Owner, _logist || _worker || _repair || _loot || _prospector);
} }
} }
@@ -190,6 +192,7 @@ namespace NecromancerTome
RemoveGenie(false); RemoveGenie(false);
CinerariumContents.CloseIfShowing(this); CinerariumContents.CloseIfShowing(this);
ZoneStop(false); ZoneStop(false);
ProspectorReset();
if (!ConnectionManager.Instance.IsServer) if (!ConnectionManager.Instance.IsServer)
{ {
return; return;
@@ -224,6 +227,7 @@ namespace NecromancerTome
_threadsDirty = true; // чанк вернётся - нити построить заново _threadsDirty = true; // чанк вернётся - нити построить заново
RemoveGenie(false); RemoveGenie(false);
CinerariumContents.CloseIfShowing(this); CinerariumContents.CloseIfShowing(this);
ProspectorReset();
} }
public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode) public override void Write(PooledBinaryWriter _bw, StreamModeWrite _eStreamMode)
@@ -447,7 +451,7 @@ namespace NecromancerTome
base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName); base.GetActivationText(_world, _blockPos, _blockValue, _entityFocusing, _activateHotkeyMarkup, _focusedTileEntityName);
string s = _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " + string s = _activateHotkeyMarkup + " " + _blockValue.Block.GetLocalizedBlockName() + " - " +
Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty"); Localization.Get(HasSkull ? "necroCinerariumStateSkull" : "necroCinerariumStateEmpty");
return IsBigZone ? s + ZoneStateText() : s; return IsBigZone ? s + ZoneStateText() : IsProspector ? s + ProspectorStateText(_world as World) : s;
} }
public override void UpdateTick(World _world) public override void UpdateTick(World _world)
@@ -752,6 +756,11 @@ namespace NecromancerTome
ZoneWork(_world); // ремонтник, лутер - CinerariumZone.cs ZoneWork(_world); // ремонтник, лутер - CinerariumZone.cs
return; return;
} }
if (IsProspector)
{
ProspectorWork(_world); // старатель - CinerariumProspector.cs
return;
}
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null; CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
if (_pending) if (_pending)
{ {
@@ -929,7 +938,7 @@ namespace NecromancerTome
logist = w != null && IsLogistWallBlock(w.GetBlock(ToWorldPos()).Block) ? 2 : 1; logist = w != null && IsLogistWallBlock(w.GetBlock(ToWorldPos()).Block) ? 2 : 1;
} }
_genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f, logist); _genie = CinerariumGenie.Spawn(ModelTransform(), IsWorker ? CinerariumGenie.WorkerDrop : 0f, logist);
if (_genie != null && IsLooter) if (_genie != null && (IsLooter || IsProspector)) // старатель - на банке лутера
{ {
_genie.GetComponent<CinerariumGenieMotion>().ForwardOverride = CinerariumGenie.LootForward; _genie.GetComponent<CinerariumGenieMotion>().ForwardOverride = CinerariumGenie.LootForward;
} }
@@ -1089,7 +1098,7 @@ namespace NecromancerTome
string n = _b?.GetBlockName(); string n = _b?.GetBlockName();
return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName || return n == BlockName || n == SkullBlockName || n == LogistBlockName || n == LogistSkullBlockName ||
n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName || n == LogistWallBlockName || n == LogistWallSkullBlockName || n == WorkerBlockName || n == WorkerSkullBlockName ||
IsRepairBlock(_b) || IsLootBlock(_b); IsRepairBlock(_b) || IsLootBlock(_b) || IsProspectorBlock(_b);
} }
public static int UrnLimit(EntityPlayer _player) public static int UrnLimit(EntityPlayer _player)
@@ -1486,7 +1495,10 @@ namespace NecromancerTome
bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block); bool worker = TEFeatureCinerarium.IsWorkerBlock(bv.Block);
bool repairUrn = TEFeatureCinerarium.IsRepairBlock(bv.Block); bool repairUrn = TEFeatureCinerarium.IsRepairBlock(bv.Block);
bool lootUrn = TEFeatureCinerarium.IsLootBlock(bv.Block); bool lootUrn = TEFeatureCinerarium.IsLootBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(repairUrn bool prospector = TEFeatureCinerarium.IsProspectorBlock(bv.Block);
ItemValue iv = ItemClass.GetItem(prospector
? (urn.HasSkull ? TEFeatureCinerarium.ProspectorSkullBlockName : TEFeatureCinerarium.ProspectorBlockName)
: repairUrn
? (urn.HasSkull ? TEFeatureCinerarium.RepairSkullBlockName : TEFeatureCinerarium.RepairBlockName) ? (urn.HasSkull ? TEFeatureCinerarium.RepairSkullBlockName : TEFeatureCinerarium.RepairBlockName)
: lootUrn : lootUrn
? (urn.HasSkull ? TEFeatureCinerarium.LootSkullBlockName : TEFeatureCinerarium.LootBlockName) ? (urn.HasSkull ? TEFeatureCinerarium.LootSkullBlockName : TEFeatureCinerarium.LootBlockName)
+7 -3
View File
@@ -278,7 +278,7 @@ namespace NecromancerTome
/// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary> /// <summary>Покачивание и поворот джинна над урной. Только вид - никакой логики.</summary>
[Preserve] [Preserve]
public class CinerariumGenieMotion : MonoBehaviour public partial class CinerariumGenieMotion : MonoBehaviour
{ {
float _yaw; float _yaw;
float _phase; float _phase;
@@ -548,7 +548,7 @@ namespace NecromancerTome
_nextSleepCheck = Time.time + 0.5f; _nextSleepCheck = Time.time + 0.5f;
// Уснул - игрок далеко или дух не среди ближайших (CinerariumLoad.AnimateNearest): замирает дома, работа идёт. // Уснул - игрок далеко или дух не среди ближайших (CinerariumLoad.AnimateNearest): замирает дома, работа идёт.
bool asleep = CinerariumGenie.PlayerFar(transform.position) || !CinerariumLoad.Animated(this); bool asleep = CinerariumGenie.PlayerFar(transform.position) || !CinerariumLoad.Animated(this);
if (asleep && !_asleep) if (asleep && !_asleep && !Digging) // старатель под землёй - не возвращать домой
{ {
_t = -1f; // полёт прерван - дело всё равно выполнится _t = -1f; // полёт прерван - дело всё равно выполнится
Gesture(0f); Gesture(0f);
@@ -558,7 +558,7 @@ namespace NecromancerTome
} }
// Виден ли: не скрыт командой; дух логиста - только в полёте (дома сидит в кадиле). Рендереры - только при смене // Виден ли: не скрыт командой; дух логиста - только в полёте (дома сидит в кадиле). Рендереры - только при смене
// (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC). // (16.10, проверка на утечки: раньше список рендереров собирался каждые 0.5 с - мусор для GC).
bool show = !CinerariumGenie.Hidden && (!Small || _t >= 0f); bool show = !CinerariumGenie.Hidden && (!Small || _t >= 0f) && !DigHidden;
if (_hiddenApplied != !show) if (_hiddenApplied != !show)
{ {
_hiddenApplied = !show; _hiddenApplied = !show;
@@ -567,6 +567,10 @@ namespace NecromancerTome
r.enabled = show; r.enabled = show;
} }
} }
if (DigUpdate(_asleep))
{
return; // старатель: спуск, под землёй, подъём (CinerariumProspector.cs)
}
if (_stuck || _asleep) if (_stuck || _asleep)
{ {
return; // замер как был return; // замер как был
+650
View File
@@ -0,0 +1,650 @@
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Цинерарий-старатель (#64, решения 20.10 и 22.10.2026): дух добывает ископаемые. В «Содержимом» две ячейки, как у рабочего:
/// череп и образец - что добывать (один из Ores). Нить к ящику (любая, как у лутера) - куда класть. Есть всё - дух отлетает на
/// ближайшую свободную клетку рядом с урной и уходит на ProspectorDepth блоков вниз, растворяясь; раз в игровой час поднимается
/// так же, кладёт добычу в ящик и уходит снова. Без зоны и вне лимита урн, как остальные некротехнологи.
///
/// Добыча (20.10: «цена - скоростью, а не ресурсом»): за игровые сутки камень, глина, песок, древесина - 5 стопок по 6000,
/// железо, свинец, уголь, селитра, сланец - 3 стопки; ходка - сутки / 24, разброс ±20%. Находки (20.10: «шанс добыть алмаз,
/// серебро и золото, как и в игре»; 22.10 - всегда, без книги «Удачный удар», латуни нет): ходка руды = столько блоков, сколько
/// даёт её выход с блока в игре (железо 105, прочие 50), на каждый - шансы игры (алмаз 0.5%, серебро 1%, золото 0.7%).
///
/// Время - игровое (worldTime), чтобы добыча за сутки не зависела от их длины. Ходка засчитывается, только пока чанк урны
/// загружен; время перескочило больше чем на час (сон, прилив, settime) - ходка не засчитана, отсчёт заново. Ящик полон или
/// открыт игроком - дух ждёт дома с добычей (почернел, если полон) и докладывает, как появится место; новой ходки нет.
/// Добыча в руках духа не сохраняется: урну подняли или чанк выгрузился - ходка пропала.
/// </summary>
public partial class TEFeatureCinerarium
{
public const string ProspectorBlockName = "necroCinerariumProspector";
public const string ProspectorSkullBlockName = "necroCinerariumProspectorSkull";
public static bool IsProspectorBlock(Block _b)
{
string n = _b?.GetBlockName();
return n == ProspectorBlockName || n == ProspectorSkullBlockName;
}
bool _prospector;
public bool IsProspector
{
get
{
_ = IsLogist; // узнаёт тип блока
return _prospector;
}
}
/// <summary>Что добывает старатель: предмет, добыча за игровые сутки, выход с блока руды в игре (0 - находок нет).</summary>
public static readonly (string item, int perDay, int perBlock)[] Ores =
{
("resourceRockSmall", 30000, 0),
("resourceClayLump", 30000, 0),
("resourceCrushedSand", 30000, 0),
("resourceWood", 30000, 0),
("resourceScrapIron", 18000, 105),
("resourceScrapLead", 18000, 50),
("resourceCoal", 18000, 50),
("resourcePotassiumNitratePowder", 18000, 50),
("resourceOilShale", 18000, 50),
};
/// <summary>Находки с блока руды - как в blocks.xml игры (тег artOfMiningLuckyStrikeHarvest).</summary>
public static readonly (string item, float prob)[] Finds =
{
("resourceRawDiamond", 0.005f),
("resourceSilverNugget", 0.01f),
("resourceGoldNugget", 0.007f),
};
/// <summary>Выключатель (консоль necroprospector off): духи остаются дома, ходок нет.</summary>
public static bool ProspectorOn = true;
/// <summary>Множитель добычи за ходку (консоль, для проверки баланса).</summary>
public static float ProspectorYield = 1f;
public static float ProspectorSpread = 0.2f;
/// <summary>На сколько блоков дух уходит вниз.</summary>
public static int ProspectorDepth = 5;
/// <summary>Ходка - игровой час (1000 единиц worldTime).</summary>
public const ulong TripTicks = 1000UL;
/// <summary>necroprospector trip - номер «внеочередной ходки»: урна, ещё не видевшая его, ходит сразу.</summary>
public static int ForceTrip;
public enum ProspectorStatus
{
NoSkull,
Tide,
Off,
NoSample,
NoChest,
Full,
Digging
}
bool _under;
ulong _tripAt;
float _nextProspect;
float _deliverAt;
int _forceSeen;
readonly List<ItemStack> _carry = new List<ItemStack>();
public static int OreIndex(ItemValue _iv)
{
string n = _iv?.ItemClass?.GetItemName();
if (n == null)
{
return -1;
}
for (int i = 0; i < Ores.Length; i++)
{
if (Ores[i].item == n)
{
return i;
}
}
return -1;
}
public static bool IsOreSample(ItemStack _s) => _s != null && !_s.IsEmpty() && OreIndex(_s.itemValue) >= 0;
/// <summary>Сколько игровых минут до конца ходки (для консоли), -1 - дух не под землёй.</summary>
public int TripMinutesLeft(World _w)
{
if (!_under || _w == null)
{
return -1;
}
return _tripAt > _w.worldTime ? (int)((_tripAt - _w.worldTime) * 60UL / TripTicks) : 0;
}
public int CarryCount
{
get
{
int n = 0;
foreach (ItemStack s in _carry)
{
n += s.count;
}
return n;
}
}
public ProspectorStatus ProspectorNow(World _world)
{
if (!HasSkull)
{
return ProspectorStatus.NoSkull;
}
if (CinerariumLoad.Tide)
{
return ProspectorStatus.Tide;
}
if (!ProspectorOn)
{
return ProspectorStatus.Off;
}
if (!IsOreSample(Sample))
{
return ProspectorStatus.NoSample;
}
if (!HasThreads)
{
return ProspectorStatus.NoChest;
}
return Stuck ? ProspectorStatus.Full : ProspectorStatus.Digging;
}
string ProspectorStateText(World _world)
{
string key = ProspectorNow(_world) switch
{
ProspectorStatus.Tide => "necroZoneUrnTide",
ProspectorStatus.NoSample => "necroProspectorNoSample",
ProspectorStatus.NoChest => "necroProspectorNoChest",
ProspectorStatus.Full => "necroZoneUrnFull",
ProspectorStatus.Digging => "necroProspectorDigging",
_ => null
};
return key != null ? ", " + Localization.Get(key) : "";
}
/// <summary>Работа старателя (сервер, раз в секунду): донести добычу, уйти под землю, ходка по часам игры.</summary>
void ProspectorWork(World _world)
{
if (Time.time < _nextProspect)
{
return;
}
_nextProspect = Time.time + 1f;
CinerariumGenieMotion motion = _genie != null ? _genie.GetComponent<CinerariumGenieMotion>() : null;
ulong now = _world.worldTime;
if (_carry.Count > 0)
{
if (Time.time < _deliverAt)
{
return; // дух ещё поднимается
}
Deliver(_world);
bool full = _carry.Count > 0;
if (full != Stuck)
{
Stuck = full;
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + (full ? " - ящик полон, дух ждёт с добычей" : " - добыча в ящике"));
}
motion?.SetStuck(full);
if (full)
{
return;
}
}
ProspectorStatus st = ProspectorNow(_world);
if (st != ProspectorStatus.Digging)
{
if (_under)
{
_under = false;
motion?.Rise();
Debug.Log("[NecromancerTome] Cinerarium: дух старателя " + ToWorldPos() + " вернулся без добычи - " + st);
}
return;
}
if (!_under)
{
_under = true;
if (_tripAt <= now)
{
_tripAt = now + TripTicks;
}
_forceSeen = ForceTrip;
motion?.Dive(DigStand(_world) - Origin.position, ProspectorDepth);
Debug.Log("[NecromancerTome] Cinerarium: дух старателя " + ToWorldPos() + " ушёл под землю за " + Sample.itemValue.ItemClass?.GetItemName());
return;
}
if (motion != null && !motion.Digging)
{
motion.Bury(DigStand(_world) - Origin.position, ProspectorDepth); // дух появился заново (чанк, прилив) - сразу под землёй
}
bool forced = _forceSeen != ForceTrip;
if (now < _tripAt && !forced)
{
return;
}
_forceSeen = ForceTrip;
if (!forced && now > _tripAt + TripTicks)
{
_tripAt = now + TripTicks; // время перескочило - ходка не засчитана
Debug.Log("[NecromancerTome] Cinerarium: у старателя " + ToWorldPos() + " время перескочило - ходка заново");
return;
}
DigTrip(OreIndex(Sample.itemValue));
_tripAt = forced ? now + TripTicks : _tripAt + TripTicks;
_under = false; // поднимается; донесёт - уйдёт снова
_deliverAt = Time.time + (motion != null ? motion.Rise() : 0f);
}
/// <summary>Добыча одной ходки - в руки духа (_carry), стопками не больше игровых.</summary>
void DigTrip(int _ore)
{
if (_ore < 0)
{
return;
}
(string item, int perDay, int perBlock) o = Ores[_ore];
float mean = o.perDay / 24f * ProspectorYield;
int n = Mathf.Max(1, Mathf.RoundToInt(mean * Random.Range(1f - ProspectorSpread, 1f + ProspectorSpread)));
Carry(o.item, n);
string log = o.item + " x" + n;
if (o.perBlock > 0)
{
int blocks = Mathf.Max(1, Mathf.RoundToInt(n / (float)o.perBlock));
foreach ((string item, float prob) f in Finds)
{
int k = 0;
for (int b = 0; b < blocks; b++)
{
if (Random.value < f.prob)
{
k++;
}
}
if (k > 0)
{
Carry(f.item, k);
log += ", " + f.item + " x" + k;
}
}
log += " (блоков руды " + blocks + ")";
}
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + " - ходка: " + log);
}
void Carry(string _item, int _n)
{
ItemValue iv = ItemClass.GetItem(_item);
if (iv == null || iv.type <= 0)
{
Debug.LogWarning("[NecromancerTome] Cinerarium: старатель - нет предмета " + _item);
return;
}
int max = Mathf.Max(1, iv.ItemClass.Stacknumber.Value);
while (_n > 0)
{
int k = Mathf.Min(_n, max);
_carry.Add(new ItemStack(iv.Clone(), k));
_n -= k;
}
}
/// <summary>Добычу - в ящики по нитям; что не влезло, остаётся у духа.</summary>
void Deliver(World _world)
{
List<TEFeatureStorage> boxes = new List<TEFeatureStorage>();
void Add(Vector3i _p)
{
TEFeatureStorage s = CinerariumWork.BoxAt(_world, _p);
if (s != null && !boxes.Contains(s))
{
boxes.Add(s);
}
}
if (HasThreadTo)
{
Add(ThreadTo);
}
foreach (Vector3i p in ThreadOut)
{
Add(p);
}
foreach (Vector3i p in ThreadFrom)
{
Add(p);
}
if (boxes.Count == 0)
{
return; // ящик открыт игроком или нитей нет - подождать
}
for (int i = _carry.Count - 1; i >= 0; i--)
{
ItemStack s = _carry[i];
CinerariumWork.Place(s, boxes);
if (s.count <= 0)
{
_carry.RemoveAt(i);
}
}
}
/// <summary>Куда дух спускается (20.10: «отлетает от урны на ближайший свободный блок, опускается на 5 блоков вниз»): из
/// восьми соседних клеток - первая свободная (воздух или растение), сначала по сторонам; нет - над урной. Низ клетки, мир.</summary>
Vector3 DigStand(World _world)
{
Vector3i urn = ToWorldPos();
Vector3i[] dirs =
{
Vector3i.forward, Vector3i.right, Vector3i.back, Vector3i.left,
new Vector3i(1, 0, 1), new Vector3i(1, 0, -1), new Vector3i(-1, 0, -1), new Vector3i(-1, 0, 1)
};
foreach (Vector3i d in dirs)
{
BlockValue bv = _world.GetBlock(urn + d);
if (bv.isair || bv.Block is BlockPlant)
{
return World.blockToTransformPos(urn + d);
}
}
return World.blockToTransformPos(urn + Vector3i.up);
}
/// <summary>Урну убрали или выгрузили - добыча в руках духа пропадает.</summary>
void ProspectorReset()
{
if (_carry.Count > 0)
{
Debug.Log("[NecromancerTome] Cinerarium: старатель " + ToWorldPos() + " - недонесённая добыча пропала (" + CarryCount + ")");
}
_carry.Clear();
_under = false;
}
}
/// <summary>Спуск духа старателя под землю и подъём (20.10: «опускается на 5 блоков вниз, постепенно становясь полностью
/// прозрачным; обратно - так же»). Яркость - MaterialPropertyBlock на рендерерах духа (материалы общие не трогаются): дух
/// светится сложением, поэтому «прозрачнее» = тусклее цвет и свечение; глаза гаснут на половине.</summary>
public partial class CinerariumGenieMotion
{
/// <summary>0 - нет; 1 - летит к месту и спускается; 2 - под землёй (не виден); 3 - поднимается и летит домой.</summary>
int _digMode;
float _digT;
Vector3 _digFrom, _digHover;
float _digDepth;
public static float SinkSeconds = 2.5f;
List<Renderer> _fadeRenderers;
static readonly MaterialPropertyBlock _fadeBlock = new MaterialPropertyBlock();
static readonly int ColorId = Shader.PropertyToID("_Color");
static readonly int EmissionId = Shader.PropertyToID("_EmissionColor");
float _fade = 1f;
/// <summary>Под землёй или на пути туда/обратно.</summary>
public bool Digging => _digMode != 0;
bool DigHidden => _digMode == 2;
/// <summary>Лететь к клетке _stand (низ клетки, координаты сцены) и уйти на _depth блоков вниз.</summary>
public void Dive(Vector3 _stand, int _depth)
{
if (transform.parent == null)
{
return;
}
_t = -1f;
Gesture(0f);
_digFrom = transform.localPosition;
_digHover = DigHover(_stand);
_digDepth = _depth;
_digMode = 1;
_digT = 0f;
}
/// <summary>Сразу под землёй (дух появился заново, а работа идёт).</summary>
public void Bury(Vector3 _stand, int _depth)
{
if (transform.parent == null)
{
return;
}
_digHover = DigHover(_stand);
_digDepth = _depth;
_digMode = 2;
transform.localPosition = _digHover + Vector3.down * _digDepth;
}
/// <summary>Подняться и вернуться к урне; через сколько секунд будет дома.</summary>
public float Rise()
{
if (_digMode == 0)
{
return 0f;
}
_digMode = 3;
_digT = 0f;
return SinkSeconds + Mathf.Max(BackSeconds, Vector3.Distance(_digHover, Home()) / FlySpeed);
}
Vector3 DigHover(Vector3 _stand)
{
return transform.parent.InverseTransformPoint(_stand) + new Vector3(0f, CinerariumGenie.OffsetY + Drop + 0.15f, 0f);
}
/// <summary>Шаг спуска/подъёма; false - дух не копает, обычное поведение.</summary>
bool DigUpdate(bool _asleep)
{
if (_digMode == 0)
{
return false;
}
if (_asleep)
{
// Уснул - без полёта: спуск сразу закончен, подъём - сразу дома.
if (_digMode == 1)
{
_digMode = 2;
}
else if (_digMode == 3)
{
_digMode = 0;
SetFade(1f);
transform.localPosition = Home();
return false;
}
return true;
}
_digT += Time.deltaTime;
Vector3 bottom = _digHover + Vector3.down * _digDepth;
float fly = Mathf.Max(OutSeconds, Vector3.Distance(_digFrom, _digHover) / FlySpeed);
if (_digMode == 1)
{
if (_digT < fly)
{
transform.localPosition = Vector3.Lerp(_digFrom, _digHover, Mathf.SmoothStep(0f, 1f, _digT / fly));
Face(_digHover - _digFrom);
SetFade(1f);
}
else if (_digT < fly + SinkSeconds)
{
float s = (_digT - fly) / SinkSeconds;
transform.localPosition = Vector3.Lerp(_digHover, bottom, Mathf.SmoothStep(0f, 1f, s));
SetFade(1f - s);
}
else
{
_digMode = 2;
transform.localPosition = bottom;
}
}
else if (_digMode == 2)
{
transform.localPosition = bottom;
}
else
{
Vector3 home = Home();
float back = Mathf.Max(BackSeconds, Vector3.Distance(_digHover, home) / FlySpeed);
if (_digT < SinkSeconds)
{
float s = _digT / SinkSeconds;
transform.localPosition = Vector3.Lerp(bottom, _digHover, Mathf.SmoothStep(0f, 1f, s));
SetFade(s);
}
else if (_digT < SinkSeconds + back)
{
SetFade(1f);
transform.localPosition = Vector3.Lerp(_digHover, home, Mathf.SmoothStep(0f, 1f, (_digT - SinkSeconds) / back));
Face(home - _digHover);
}
else
{
_digMode = 0;
SetFade(1f);
return false;
}
}
transform.localRotation = Quaternion.Euler(0f, _yaw, 0f);
return true;
}
/// <summary>Яркость духа долей _k (1 - как обычно, свойства сняты).</summary>
void SetFade(float _k)
{
_k = Mathf.Clamp01(_k);
if (Mathf.Abs(_k - _fade) < 0.01f && (_k < 1f || _fade >= 1f))
{
return;
}
_fade = _k;
if (_fadeRenderers == null)
{
_fadeRenderers = new List<Renderer>(GetComponentsInChildren<Renderer>(true));
}
foreach (Renderer r in _fadeRenderers)
{
if (r == null)
{
continue;
}
if (r.name.Contains("eye"))
{
r.forceRenderingOff = _k < 0.5f;
continue;
}
if (_k >= 1f)
{
r.SetPropertyBlock(null);
continue;
}
Material m = r.sharedMaterial;
if (m == null)
{
continue;
}
r.GetPropertyBlock(_fadeBlock);
if (m.HasProperty(ColorId))
{
_fadeBlock.SetColor(ColorId, m.GetColor(ColorId) * _k);
}
if (m.HasProperty(EmissionId))
{
_fadeBlock.SetColor(EmissionId, m.GetColor(EmissionId) * _k);
}
r.SetPropertyBlock(_fadeBlock);
}
}
}
/// <summary>Консоль старателя: выключатель, внеочередная ходка, множитель добычи, состояние урн.</summary>
[Preserve]
public class ConsoleCmdNecroProspector : ConsoleCmdAbstract
{
public override bool IsExecuteOnClient => true;
public override bool AllowedInMainMenu => false;
public override string[] getCommands()
{
return new string[] { "necroprospector" };
}
public override string getDescription()
{
return "Necromancer's Tome: Prospector Cinerarium - switch, extra trip, yield multiplier, status.";
}
public override string getHelp()
{
return "necroprospector - status of every loaded prospector urn\n" +
"necroprospector on|off - off: spirits stay home, no trips\n" +
"necroprospector trip - every digging spirit comes back with a trip right now\n" +
"necroprospector yield <x> - trip size multiplier (1 = as designed)\n" +
"necroprospector spread <x> - random spread of a trip (0.2 = ±20%)\n" +
"necroprospector depth <n> - how many blocks the spirit sinks";
}
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
SdtdConsole con = SingletonMonoBehaviour<SdtdConsole>.Instance;
World w = GameManager.Instance?.World;
if (_params.Count >= 1)
{
string cmd = _params[0].ToLowerInvariant();
float v = 0f;
bool num = _params.Count >= 2 && float.TryParse(_params[1], NumberStyles.Float, CultureInfo.InvariantCulture, out v);
switch (cmd)
{
case "on":
case "off":
TEFeatureCinerarium.ProspectorOn = cmd == "on";
break;
case "trip":
TEFeatureCinerarium.ForceTrip++;
break;
case "yield" when num:
TEFeatureCinerarium.ProspectorYield = Mathf.Max(0f, v);
break;
case "spread" when num:
TEFeatureCinerarium.ProspectorSpread = Mathf.Clamp01(v);
break;
case "depth" when num:
TEFeatureCinerarium.ProspectorDepth = Mathf.Clamp((int)v, 1, 30);
break;
default:
con.Output("necroprospector: unknown '" + _params[0] + "' - see 'help necroprospector'");
return;
}
}
con.Output("necroprospector: " + (TEFeatureCinerarium.ProspectorOn ? "on" : "OFF") + ", yield x" + TEFeatureCinerarium.ProspectorYield.ToString("F2", CultureInfo.InvariantCulture) +
", spread " + TEFeatureCinerarium.ProspectorSpread.ToString("F2", CultureInfo.InvariantCulture) + ", depth " + TEFeatureCinerarium.ProspectorDepth);
if (w == null)
{
return;
}
foreach (Vector3i p in CinerariumRegistry.All())
{
TEFeatureCinerarium u = CinerariumThreads.UrnAt(w, p);
if (u == null || !u.IsProspector)
{
continue;
}
int left = u.TripMinutesLeft(w);
con.Output(" " + p + ": " + u.ProspectorNow(w) + ", sample " + (u.Sample == null || u.Sample.IsEmpty() ? "-" : u.Sample.itemValue.ItemClass?.GetItemName()) +
(left >= 0 ? ", trip in " + left + " game min" : "") + (u.CarryCount > 0 ? ", carrying " + u.CarryCount : ""));
}
}
}
}
+27 -3
View File
@@ -10,6 +10,7 @@ namespace NecromancerTome
/// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли /// в ячейке остаётся один череп, всё остальное (лишние черепа, чужие предметы) - назад в рюкзак, и урна узнаёт, есть ли
/// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя. /// в ней череп. Череп - обычный предмет (ItemClass): он уже в сейвах, менять ему класс на модификацию нельзя.
/// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе). /// У урны рабочего (18.10) ячеек две: вторая - образец (любой предмет, стопка целиком - сколько держать в запасе).
/// У старателя (22.10) вторая ячейка - что добывать: один предмет из TEFeatureCinerarium.Ores, лишнее и чужое - назад.
/// </summary> /// </summary>
public static class CinerariumContents public static class CinerariumContents
{ {
@@ -31,12 +32,12 @@ namespace NecromancerTome
} }
_urn = _u; _urn = _u;
_player = _p; _player = _p;
_bag = new Bag(new Vector2i(_u.IsWorker ? 2 : 1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null); _bag = new Bag(new Vector2i(_u.IsWorker || _u.IsProspector ? 2 : 1, 1), XUiC_ItemStack.StackLocationTypes.LootContainer, null);
if (_u.HasSkull && skull != null && skull.type > 0) if (_u.HasSkull && skull != null && skull.type > 0)
{ {
_bag.SetSlot(0, new ItemStack(skull, 1)); _bag.SetSlot(0, new ItemStack(skull, 1));
} }
if (_u.IsWorker && _u.Sample != null && !_u.Sample.IsEmpty()) if ((_u.IsWorker || _u.IsProspector) && _u.Sample != null && !_u.Sample.IsEmpty())
{ {
_bag.SetSlot(1, _u.Sample.Clone()); _bag.SetSlot(1, _u.Sample.Clone());
} }
@@ -59,7 +60,7 @@ namespace NecromancerTome
{ {
return; return;
} }
if (_urn.IsWorker && _bag.ItemGrid.items.Length > 1) if ((_urn.IsWorker || _urn.IsProspector) && _bag.ItemGrid.items.Length > 1)
{ {
ItemStack smp = _bag.ItemGrid.items[1]; ItemStack smp = _bag.ItemGrid.items[1];
ItemStack first = _bag.ItemGrid.items[0]; ItemStack first = _bag.ItemGrid.items[0];
@@ -70,6 +71,29 @@ namespace NecromancerTome
_bag.SetSlot(1, ItemStack.Empty.Clone()); _bag.SetSlot(1, ItemStack.Empty.Clone());
smp = _bag.ItemGrid.items[1]; smp = _bag.ItemGrid.items[1];
} }
if (_urn.IsProspector && smp != null && !smp.IsEmpty())
{
// Старатель: образец - один предмет из списка добычи; чужое целиком, лишние штуки - назад.
if (!TEFeatureCinerarium.IsOreSample(smp))
{
GiveBack(smp.Clone());
_bag.SetSlot(1, ItemStack.Empty.Clone());
if (_player != null)
{
GameManager.ShowTooltip(_player, Localization.Get("necroProspectorBadSample"));
}
}
else if (smp.count > 1)
{
ItemStack back = smp.Clone();
back.count = smp.count - 1;
GiveBack(back);
ItemStack one = smp.Clone();
one.count = 1;
_bag.SetSlot(1, one);
}
smp = _bag.ItemGrid.items[1];
}
_urn.SetSample(smp != null && !smp.IsEmpty() ? smp.Clone() : ItemStack.Empty.Clone()); _urn.SetSample(smp != null && !smp.IsEmpty() ? smp.Clone() : ItemStack.Empty.Clone());
} }
ItemStack s = _bag.ItemGrid.items[0]; ItemStack s = _bag.ItemGrid.items[0];
+1 -1
View File
@@ -20,7 +20,7 @@ namespace NecromancerTome
static bool Ours(string _name) static bool Ours(string _name)
{ {
return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab" || _name == "necroCinerariumWorkerPrefab" || _name.StartsWith("necroCinerariumLogist") || _name == "necroCinerariumRepairPrefab" || _name == "necroCinerariumLootPrefab"; return _name == "necroBookMindPrefab" || _name == "necroKnifeDecorPrefab" || _name == "necroMortarPrefab" || _name.StartsWith("necroBloodRose") || _name.StartsWith("necroPoster") || _name == "necroBookDoorPrefab" || _name.StartsWith("necroSkullDecor") || _name == "necroCinerariumPrefab" || _name == "necroCinerariumWorkerPrefab" || _name.StartsWith("necroCinerariumLogist") || _name == "necroCinerariumRepairPrefab" || _name == "necroCinerariumLootPrefab" || _name == "necroCinerariumProspectorPrefab";
} }
public static void Postfix(Transform __result) public static void Postfix(Transform __result)
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

After

Width:  |  Height:  |  Size: 623 KiB