-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
ExamplePerson.cs
419 lines (355 loc) · 18.7 KB
/
ExamplePerson.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
using ExampleMod.Common;
using ExampleMod.Common.Configs;
using ExampleMod.Content.Biomes;
using ExampleMod.Content.Dusts;
using ExampleMod.Content.EmoteBubbles;
using ExampleMod.Content.Items;
using ExampleMod.Content.Items.Accessories;
using ExampleMod.Content.Items.Armor;
using ExampleMod.Content.Projectiles;
using ExampleMod.Content.Tiles;
using ExampleMod.Content.Tiles.Furniture;
using ExampleMod.Content.Walls;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent;
using Terraria.GameContent.Bestiary;
using Terraria.GameContent.ItemDropRules;
using Terraria.GameContent.Personalities;
using Terraria.GameContent.UI;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
using Terraria.Utilities;
namespace ExampleMod.Content.NPCs
{
// [AutoloadHead] and NPC.townNPC are extremely important and absolutely both necessary for any Town NPC to work at all.
[AutoloadHead]
public class ExamplePerson : ModNPC
{
public const string ShopName = "Shop";
public int NumberOfTimesTalkedTo = 0;
private static int ShimmerHeadIndex;
private static Profiles.StackedNPCProfile NPCProfile;
public override void Load() {
// Adds our Shimmer Head to the NPCHeadLoader.
ShimmerHeadIndex = Mod.AddNPCHeadTexture(Type, Texture + "_Shimmer_Head");
}
public override void SetStaticDefaults() {
Main.npcFrameCount[Type] = 25; // The total amount of frames the NPC has
NPCID.Sets.ExtraFramesCount[Type] = 9; // Generally for Town NPCs, but this is how the NPC does extra things such as sitting in a chair and talking to other NPCs. This is the remaining frames after the walking frames.
NPCID.Sets.AttackFrameCount[Type] = 4; // The amount of frames in the attacking animation.
NPCID.Sets.DangerDetectRange[Type] = 700; // The amount of pixels away from the center of the NPC that it tries to attack enemies.
NPCID.Sets.AttackType[Type] = 0; // The type of attack the Town NPC performs. 0 = throwing, 1 = shooting, 2 = magic, 3 = melee
NPCID.Sets.AttackTime[Type] = 90; // The amount of time it takes for the NPC's attack animation to be over once it starts.
NPCID.Sets.AttackAverageChance[Type] = 30; // The denominator for the chance for a Town NPC to attack. Lower numbers make the Town NPC appear more aggressive.
NPCID.Sets.HatOffsetY[Type] = 4; // For when a party is active, the party hat spawns at a Y offset.
NPCID.Sets.ShimmerTownTransform[NPC.type] = true; // This set says that the Town NPC has a Shimmered form. Otherwise, the Town NPC will become transparent when touching Shimmer like other enemies.
NPCID.Sets.ShimmerTownTransform[Type] = true; // Allows for this NPC to have a different texture after touching the Shimmer liquid.
// Connects this NPC with a custom emote.
// This makes it when the NPC is in the world, other NPCs will "talk about him".
// By setting this you don't have to override the PickEmote method for the emote to appear.
NPCID.Sets.FaceEmote[Type] = ModContent.EmoteBubbleType<ExamplePersonEmote>();
// Influences how the NPC looks in the Bestiary
NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers = new NPCID.Sets.NPCBestiaryDrawModifiers() {
Velocity = 1f, // Draws the NPC in the bestiary as if its walking +1 tiles in the x direction
Direction = 1 // -1 is left and 1 is right. NPCs are drawn facing the left by default but ExamplePerson will be drawn facing the right
// Rotation = MathHelper.ToRadians(180) // You can also change the rotation of an NPC. Rotation is measured in radians
// If you want to see an example of manually modifying these when the NPC is drawn, see PreDraw
};
NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);
// Set Example Person's biome and neighbor preferences with the NPCHappiness hook. You can add happiness text and remarks with localization (See an example in ExampleMod/Localization/en-US.lang).
// NOTE: The following code uses chaining - a style that works due to the fact that the SetXAffection methods return the same NPCHappiness instance they're called on.
NPC.Happiness
.SetBiomeAffection<ForestBiome>(AffectionLevel.Like) // Example Person prefers the forest.
.SetBiomeAffection<SnowBiome>(AffectionLevel.Dislike) // Example Person dislikes the snow.
.SetBiomeAffection<ExampleSurfaceBiome>(AffectionLevel.Love) // Example Person likes the Example Surface Biome
.SetNPCAffection(NPCID.Dryad, AffectionLevel.Love) // Loves living near the dryad.
.SetNPCAffection(NPCID.Guide, AffectionLevel.Like) // Likes living near the guide.
.SetNPCAffection(NPCID.Merchant, AffectionLevel.Dislike) // Dislikes living near the merchant.
.SetNPCAffection(NPCID.Demolitionist, AffectionLevel.Hate) // Hates living near the demolitionist.
; // < Mind the semicolon!
// This creates a "profile" for ExamplePerson, which allows for different textures during a party and/or while the NPC is shimmered.
NPCProfile = new Profiles.StackedNPCProfile(
new Profiles.DefaultNPCProfile(Texture, NPCHeadLoader.GetHeadSlot(HeadTexture), Texture + "_Party"),
new Profiles.DefaultNPCProfile(Texture + "_Shimmer", ShimmerHeadIndex, Texture + "_Shimmer_Party")
);
}
public override void SetDefaults() {
NPC.townNPC = true; // Sets NPC to be a Town NPC
NPC.friendly = true; // NPC Will not attack player
NPC.width = 18;
NPC.height = 40;
NPC.aiStyle = 7;
NPC.damage = 10;
NPC.defense = 15;
NPC.lifeMax = 250;
NPC.HitSound = SoundID.NPCHit1;
NPC.DeathSound = SoundID.NPCDeath1;
NPC.knockBackResist = 0.5f;
AnimationType = NPCID.Guide;
}
public override void SetBestiary(BestiaryDatabase database, BestiaryEntry bestiaryEntry) {
// We can use AddRange instead of calling Add multiple times in order to add multiple items at once
bestiaryEntry.Info.AddRange(new IBestiaryInfoElement[] {
// Sets the preferred biomes of this town NPC listed in the bestiary.
// With Town NPCs, you usually set this to what biome it likes the most in regards to NPC happiness.
BestiaryDatabaseNPCsPopulator.CommonTags.SpawnConditions.Biomes.Surface,
// Sets your NPC's flavor text in the bestiary.
new FlavorTextBestiaryInfoElement("Hailing from a mysterious greyscale cube world, the Example Person is here to help you understand everything about tModLoader."),
// You can add multiple elements if you really wanted to
// You can also use localization keys (see Localization/en-US.lang)
new FlavorTextBestiaryInfoElement("Mods.ExampleMod.Bestiary.ExamplePerson")
});
}
// The PreDraw hook is useful for drawing things before our sprite is drawn or running code before the sprite is drawn
// Returning false will allow you to manually draw your NPC
public override bool PreDraw(SpriteBatch spriteBatch, Vector2 screenPos, Color drawColor) {
// This code slowly rotates the NPC in the bestiary
// (simply checking NPC.IsABestiaryIconDummy and incrementing NPC.Rotation won't work here as it gets overridden by drawModifiers.Rotation each tick)
if (NPCID.Sets.NPCBestiaryDrawOffset.TryGetValue(Type, out NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers)) {
drawModifiers.Rotation += 0.001f;
// Replace the existing NPCBestiaryDrawModifiers with our new one with an adjusted rotation
NPCID.Sets.NPCBestiaryDrawOffset.Remove(Type);
NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);
}
return true;
}
public override void HitEffect(NPC.HitInfo hit) {
int num = NPC.life > 0 ? 1 : 5;
for (int k = 0; k < num; k++) {
Dust.NewDust(NPC.position, NPC.width, NPC.height, ModContent.DustType<Sparkle>());
}
// Create gore when the NPC is killed.
if (Main.netMode != NetmodeID.Server && NPC.life <= 0) {
// Retrieve the gore types. This NPC has shimmer and party variants for head, arm, and leg gore. (12 total gores)
string variant = "";
if (NPC.IsShimmerVariant) variant += "_Shimmer";
if (NPC.altTexture == 1) variant += "_Party";
int hatGore = NPC.GetPartyHatGore();
int headGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Head").Type;
int armGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Arm").Type;
int legGore = Mod.Find<ModGore>($"{Name}_Gore{variant}_Leg").Type;
// Spawn the gores. The positions of the arms and legs are lowered for a more natural look.
if (hatGore > 0) {
Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, hatGore);
}
Gore.NewGore(NPC.GetSource_Death(), NPC.position, NPC.velocity, headGore, 1f);
Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 20), NPC.velocity, armGore);
Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 20), NPC.velocity, armGore);
Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 34), NPC.velocity, legGore);
Gore.NewGore(NPC.GetSource_Death(), NPC.position + new Vector2(0, 34), NPC.velocity, legGore);
}
}
public override bool CanTownNPCSpawn(int numTownNPCs) { // Requirements for the town NPC to spawn.
foreach (var player in Main.ActivePlayers) {
// Player has to have either an ExampleItem or an ExampleBlock in order for the NPC to spawn
if (player.inventory.Any(item => item.type == ModContent.ItemType<ExampleItem>() || item.type == ModContent.ItemType<Items.Placeable.ExampleBlock>())) {
return true;
}
}
return false;
}
// Example Person needs a house built out of ExampleMod tiles. You can delete this whole method in your townNPC for the regular house conditions.
public override bool CheckConditions(int left, int right, int top, int bottom) {
int score = 0;
for (int x = left; x <= right; x++) {
for (int y = top; y <= bottom; y++) {
int type = Main.tile[x, y].TileType;
if (type == ModContent.TileType<ExampleBlock>() || type == ModContent.TileType<ExampleChair>() || type == ModContent.TileType<ExampleWorkbench>() || type == ModContent.TileType<ExampleBed>() || type == ModContent.TileType<ExampleDoorOpen>() || type == ModContent.TileType<ExampleDoorClosed>()) {
score++;
}
if (Main.tile[x, y].WallType == ModContent.WallType<ExampleWall>()) {
score++;
}
}
}
return score >= ((right - left) * (bottom - top)) / 2;
}
public override ITownNPCProfile TownNPCProfile() {
return NPCProfile;
}
public override List<string> SetNPCNameList() {
return new List<string>() {
"Someone",
"Somebody",
"Blocky",
"Colorless"
};
}
public override void FindFrame(int frameHeight) {
/*npc.frame.Width = 40;
if (((int)Main.time / 10) % 2 == 0)
{
npc.frame.X = 40;
}
else
{
npc.frame.X = 0;
}*/
}
public override string GetChat() {
WeightedRandom<string> chat = new WeightedRandom<string>();
int partyGirl = NPC.FindFirstNPC(NPCID.PartyGirl);
if (partyGirl >= 0 && Main.rand.NextBool(4)) {
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.PartyGirlDialogue", Main.npc[partyGirl].GivenName));
}
// These are things that the NPC has a chance of telling you when you talk to it.
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.StandardDialogue1"));
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.StandardDialogue2"));
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.StandardDialogue3"));
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.StandardDialogue4"));
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.CommonDialogue"), 5.0);
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.RareDialogue"), 0.1);
NumberOfTimesTalkedTo++;
if (NumberOfTimesTalkedTo >= 10) {
//This counter is linked to a single instance of the NPC, so if ExamplePerson is killed, the counter will reset.
chat.Add(Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.TalkALot"));
}
string chosenChat = chat; // chat is implicitly cast to a string. This is where the random choice is made.
// Here is some additional logic based on the chosen chat line. In this case, we want to display an item in the corner for StandardDialogue4.
if (chosenChat == Language.GetTextValue("Mods.ExampleMod.Dialogue.ExamplePerson.StandardDialogue4")) {
// Main.npcChatCornerItem shows a single item in the corner, like the Angler Quest chat.
Main.npcChatCornerItem = ItemID.HiveBackpack;
}
return chosenChat;
}
public override void SetChatButtons(ref string button, ref string button2) { // What the chat buttons are when you open up the chat UI
button = Language.GetTextValue("LegacyInterface.28");
button2 = "Awesomeify";
if (Main.LocalPlayer.HasItem(ItemID.HiveBackpack)) {
button = "Upgrade " + Lang.GetItemNameValue(ItemID.HiveBackpack);
}
}
public override void OnChatButtonClicked(bool firstButton, ref string shop) {
if (firstButton) {
// We want 3 different functionalities for chat buttons, so we use HasItem to change button 1 between a shop and upgrade action.
if (Main.LocalPlayer.HasItem(ItemID.HiveBackpack)) {
SoundEngine.PlaySound(SoundID.Item37); // Reforge/Anvil sound
Main.npcChatText = $"I upgraded your {Lang.GetItemNameValue(ItemID.HiveBackpack)} to a {Lang.GetItemNameValue(ModContent.ItemType<WaspNest>())}";
int hiveBackpackItemIndex = Main.LocalPlayer.FindItem(ItemID.HiveBackpack);
var entitySource = NPC.GetSource_GiftOrReward();
Main.LocalPlayer.inventory[hiveBackpackItemIndex].TurnToAir();
Main.LocalPlayer.QuickSpawnItem(entitySource, ModContent.ItemType<WaspNest>());
return;
}
shop = ShopName; // Name of the shop tab we want to open.
}
}
// Not completely finished, but below is what the NPC will sell
public override void AddShops() {
var npcShop = new NPCShop(Type, ShopName)
.Add<ExampleItem>()
//.Add<EquipMaterial>()
//.Add<BossItem>()
.Add(new Item(ModContent.ItemType<Items.Placeable.Furniture.ExampleWorkbench>()) { shopCustomPrice = Item.buyPrice(copper: 15) }) // This example sets a custom price, ExampleNPCShop.cs has more info on custom prices and currency.
.Add<Items.Placeable.Furniture.ExampleChair>()
.Add<Items.Placeable.Furniture.ExampleDoor>()
.Add<Items.Placeable.Furniture.ExampleBed>()
.Add<Items.Placeable.Furniture.ExampleChest>()
.Add<Items.Tools.ExamplePickaxe>()
.Add<Items.Tools.ExampleHamaxe>()
.Add<Items.Consumables.ExampleHealingPotion>(new Condition("Mods.ExampleMod.Conditions.PlayerHasLifeforceBuff", () => Main.LocalPlayer.HasBuff(BuffID.Lifeforce)))
.Add<Items.Weapons.ExampleSword>(Condition.MoonPhasesQuarter0)
//.Add<ExampleGun>(Condition.MoonPhasesQuarter1)
.Add<Items.Ammo.ExampleBullet>(Condition.MoonPhasesQuarter1)
.Add<Items.Weapons.ExampleStaff>(ExampleConditions.DownedMinionBoss)
.Add<ExampleOnBuyItem>()
.Add<Items.Weapons.ExampleYoyo>(Condition.IsNpcShimmered); // Let's sell an yoyo if this NPC is shimmered!
if (ModContent.GetInstance<ExampleModConfig>().ExampleWingsToggle) {
npcShop.Add<ExampleWings>(ExampleConditions.InExampleBiome);
}
if (ModContent.TryFind("SummonersAssociation/BloodTalisman", out ModItem bloodTalisman)) {
npcShop.Add(bloodTalisman.Type);
}
npcShop.Register(); // Name of this shop tab
}
public override void ModifyActiveShop(string shopName, Item[] items) {
foreach (Item item in items) {
// Skip 'air' items and null items.
if (item == null || item.type == ItemID.None) {
continue;
}
// If NPC is shimmered then reduce all prices by 50%.
if (NPC.IsShimmerVariant) {
int value = item.shopCustomPrice ?? item.value;
item.shopCustomPrice = value / 2;
}
}
}
public override void ModifyNPCLoot(NPCLoot npcLoot) {
npcLoot.Add(ItemDropRule.Common(ModContent.ItemType<ExampleCostume>()));
}
// Make this Town NPC teleport to the King and/or Queen statue when triggered. Return toKingStatue for only King Statues. Return !toKingStatue for only Queen Statues. Return true for both.
public override bool CanGoToStatue(bool toKingStatue) => true;
// Make something happen when the npc teleports to a statue. Since this method only runs server side, any visual effects like dusts or gores have to be synced across all clients manually.
public override void OnGoToStatue(bool toKingStatue) {
if (Main.netMode == NetmodeID.Server) {
ModPacket packet = Mod.GetPacket();
packet.Write((byte)ExampleMod.MessageType.ExampleTeleportToStatue);
packet.Write((byte)NPC.whoAmI);
packet.Send();
}
else {
StatueTeleport();
}
}
// Create a square of pixels around the NPC on teleport.
public void StatueTeleport() {
for (int i = 0; i < 30; i++) {
Vector2 position = Main.rand.NextVector2Square(-20, 21);
if (Math.Abs(position.X) > Math.Abs(position.Y)) {
position.X = Math.Sign(position.X) * 20;
}
else {
position.Y = Math.Sign(position.Y) * 20;
}
Dust.NewDustPerfect(NPC.Center + position, ModContent.DustType<Sparkle>(), Vector2.Zero).noGravity = true;
}
}
public override void TownNPCAttackStrength(ref int damage, ref float knockback) {
damage = 20;
knockback = 4f;
}
public override void TownNPCAttackCooldown(ref int cooldown, ref int randExtraCooldown) {
cooldown = 30;
randExtraCooldown = 30;
}
public override void TownNPCAttackProj(ref int projType, ref int attackDelay) {
projType = ModContent.ProjectileType<SparklingBall>();
attackDelay = 1;
}
public override void TownNPCAttackProjSpeed(ref float multiplier, ref float gravityCorrection, ref float randomOffset) {
multiplier = 12f;
randomOffset = 2f;
// SparklingBall is not affected by gravity, so gravityCorrection is left alone.
}
public override void LoadData(TagCompound tag) {
NumberOfTimesTalkedTo = tag.GetInt("numberOfTimesTalkedTo");
}
public override void SaveData(TagCompound tag) {
tag["numberOfTimesTalkedTo"] = NumberOfTimesTalkedTo;
}
// Let the NPC "talk about" minion boss
public override int? PickEmote(Player closestPlayer, List<int> emoteList, WorldUIAnchor otherAnchor) {
// By default this NPC will have a chance to use the Minion Boss Emote even if Minion Boss is not downed yet
int type = ModContent.EmoteBubbleType<MinionBossEmote>();
// If the NPC is talking to the Demolitionist, it will be more likely to react with angry emote
if (otherAnchor.entity is NPC { type: NPCID.Demolitionist }) {
type = EmoteID.EmotionAnger;
}
// Make the selection more likely by adding it to the list multiple times
for (int i = 0; i < 4; i++) {
emoteList.Add(type);
}
// Use this or return null if you don't want to override the emote selection totally
return base.PickEmote(closestPlayer, emoteList, otherAnchor);
}
}
}