Town Life
NPCs carry lanterns about in the evening, tell you about animals nearby, and pigs follow anyone holding a carrot.
NPCs carry lanterns about in the evening, tell you about animals nearby, and pigs follow anyone holding a carrot.
Mod id example.townlife, side both.
A code mod: build it with dotnet build (see Making mods) and put the output folder in the game's Mods folder.
mod.json
{
"id": "example.townlife",
"name": "Town Life",
"version": "1.0",
"author": "Seeds & Circuits",
"side": "both",
"description": "NPCs carry lanterns about in the evening, tell you about animals nearby, and pigs follow anyone holding a carrot."
}
TownLife.cs
using SeedsAndCircuits.Animals;
using SeedsAndCircuits.Mods;
using SeedsAndCircuits.Player;
using SeedsAndCircuits.Sky;
using SeedsAndCircuits.Town;
using UnityEngine;
using Inventory = SeedsAndCircuits.Inventory.Inventory;
namespace TownLife
{
/// <summary>
/// Behaviour for NPCs and animals: town folk light lanterns in the evening (an NPC job), have something to say about
/// animals nearby (a dialogue option) and a line of gossip, and pigs follow anyone holding a carrot (an animal
/// behaviour). Jobs and behaviours run on the host; the dialogue runs on each player's machine.
/// </summary>
public sealed class TownLife : Mod
{
public override void OnLoad()
{
// Every NPC. Shopkeepers keep their shop job in the day: the lantern job wraps it (the game's ShopJob).
AddNpcJob(null, npc => new LanternJob(npc.Type != null && npc.Type.runsShop ? new ShopJob(npc.Type) : null));
AddDialogueOption("Seen any animals about?", npc =>
{
int count = 0;
foreach (AnimalAgent a in Object.FindObjectsByType<AnimalAgent>())
if (!a.IsDead && Vector3.Distance(a.transform.position, npc.transform.position) < 80f)
count++;
return count == 0 ? "Not a hoof or a feather. Quiet as anything." :
count == 1 ? "Just the one, out past the houses." : $"I've counted {count} of them around here today.";
});
AddDialogueLine(null, "They say a carrot will lead a pig anywhere.");
AddAnimalBehaviour("pig", pig => new CarrotFollow());
}
}
/// <summary>
/// From 19:00 to 21:00 an NPC carries a lantern about and says so, then puts it away. The rest of the day it does
/// its usual job (<paramref name="usual"/>, e.g. keeping shop; null = strolling about town).
/// </summary>
sealed class LanternJob : INpcJob
{
readonly INpcJob _usual;
bool _lit;
public LanternJob(INpcJob usual) => _usual = usual;
Vector3 _spot;
float _nextSpotAt;
public bool WantsToWork(TownNpc npc)
{
float hour = SkySystem.Instance != null ? SkySystem.Instance.TimeOfDay : 12f;
bool evening = hour >= 19f && hour < 21f && !npc.IsSleeping;
if (!evening && _lit)
{
_lit = false;
npc.Hold(null);
}
return evening || _usual != null && _usual.WantsToWork(npc);
}
public void Work(TownNpc npc, float deltaTime)
{
float hour = SkySystem.Instance != null ? SkySystem.Instance.TimeOfDay : 12f;
if (!(hour >= 19f && hour < 21f))
{
_usual?.Work(npc, deltaTime);
return;
}
if (!_lit)
{
_lit = true;
npc.Hold("lantern");
npc.Say("Time to light the lamps!");
}
// Strolls from spot to spot nearby, holding the lantern up.
if (Time.time >= _nextSpotAt)
{
_nextSpotAt = Time.time + Random.Range(8f, 15f);
Vector2 r = Random.insideUnitCircle * 8f;
_spot = npc.transform.position + new Vector3(r.x, 0f, r.y);
}
npc.WalkTo(_spot);
}
}
/// <summary>A pig follows the nearest player holding a carrot (within 15 m), keeping a step behind.</summary>
sealed class CarrotFollow : IAnimalBehaviour
{
public bool Think(AnimalAgent pig, float now)
{
PlayerSpawn best = null;
float bestDistance = 15f;
foreach (PlayerSpawn player in PlayerSpawn.All)
{
if (player == null || !player.TryGetComponent(out Inventory inventory) || inventory.HeldItemId != "carrot")
continue;
float d = Vector3.Distance(player.Feet, pig.transform.position);
if (d < bestDistance)
{
bestDistance = d;
best = player;
}
}
if (best == null)
return false; // nobody with a carrot: the pig goes on as usual
if (bestDistance > 2.5f)
{
Vector3 behind = best.Feet + (pig.transform.position - best.Feet).normalized * 2f;
pig.GoTo(behind, run: bestDistance > 8f);
}
else
{
pig.Stop();
pig.FaceTowards(best.Feet);
}
return true;
}
}
}
TownLife.csproj
<Project Sdk="Microsoft.NET.Sdk">
<!-- Build settings and game references: ../Mod.props. Output: bin/Town Life/ -->
<PropertyGroup>
<AssemblyName>TownLife</AssemblyName>
<ModFolder>Town Life</ModFolder>
</PropertyGroup>
<Import Project="../Mod.props" />
</Project>