Modding

Town Life

NPCs carry lanterns about in the evening, tell you about animals nearby, and pigs follow anyone holding a carrot.

Updated 11 Oct 2026

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>