Advanced rpg looting chest in typescript
In this guide you will learn to create a channeling chest, which drops loot. The tricky part is to scale that item, because "Scale" is not supported, and to keep that item while channeling. Also you want to replace it with a opened chest model after successfully opening it. If the channeling fails, you also want to keep the chest. It also should keep the scale and rotation. Also the chest we are using has two different material sets and of course we want to support "Skin" as well.
So we gotta implement that. Let's go.
In our example we use models/props_generic/chest_treasure_02.vmdl and models\props_generic\chest_treasure_02_open.vmdl, which both have a golden and brown material. 0 is brown, 1 is gold.
"DOTAAbilities"
{
"item_treasure_chest_2"
{
"BaseClass" "item_lua"
"Model" "models/props_generic/chest_treasure_02.vmdl"
"ScriptFile" "item_treasure_chest_2.lua"
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_CHANNELLED"
"AbilityTextureName" "item_desolator"
"ItemShareability" "ITEM_FULLY_SHAREABLE"
"ItemKillable" "0"
"ItemSellable" "0"
"ItemPurchasable" "0"
"ItemDroppable" "1"
"ItemPermanent" "0"
"ItemCost" "99999"
"AbilityCooldown" "0.0" // Ability cooldown must be 0
"AbilityChannelTime" "2.5"
"AbilityUnitTargetType" "DOTA_UNIT_TARGET_HERO"
"OnlyPlayerHeroPickup" "1"
"CreepHeroPickup" "1"
"DisplayOverheadAlertOnReceived" "0" // Show no item acquired overhead effect
"ItemCastOnPickup" "1" // Start channeling on pickup
// Custom Values
// -------------
"ReplaceOnOpen" "models\props_generic\chest_treasure_02_open.vmdl"
"Skin" "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
Instead of item_desolator, you should use a better icon 😃
// registerAbility() and BaseItem is provided by dota_ts_adapter and needs to be imported.
@registerAbility()
class item_treasure_chest_2 extends BaseItem {
position!: Vector;
angles!: Vector;
kv!: Record<string, string>;
channelingPlaceholder!: CBaseAnimating;
Spawn() {
if (IsServer()) {
this.kv = this.GetAbilityKeyValues() as Record<string, string>;
this.HandleSkinAndScale();
}
}
private HandleSkinAndScale() {
const kvSkin = this.GetSkin();
const kvScale = this.GetScale();
if (kvSkin == 0 && kvScale == 1) {
return;
}
// Container is not created by engine at Spawn() time
Timers.CreateTimer(0.01, () => {
const container = this.GetContainer();
if (container) {
container.SetSkin(kvSkin);
container.SetModelScale(kvScale);
}
});
}
// Since we channel this ability on pickup it isnt the same as the item icon, so we need to set it here.
// When using a custom icon, make sure to support both formats (spellicon/icon)
GetAbilityTextureName() {
return GetAbilityTextureNameForAbility(this.GetName());
}
// OnSpellStart gets called twice for some reason, thats why we check if position and placeholder are set
OnSpellStart() {
if (IsClient()) {
return;
}
if (!this.position) {
this.position = this.GetContainer()!.GetOrigin();
this.angles = this.GetContainer()!.GetAnglesAsVector();
}
if (!this.channelingPlaceholder) {
this.channelingPlaceholder = this.SpawnReplacementChests(this.GetChestModel());
}
}
OnChannelFinish(interrupted: boolean) {
if (IsClient()) {
return;
}
if (interrupted) {
this.RedropChest();
this.DeleteChest();
return;
}
this.OnChestOpen();
}
OnChestOpen() {
print('Opened chest');
this.CreateLoot();
this.SpawnReplacementChests(this.GetChestOpenModel());
this.DeleteChest();
}
// spawned chests do not have a collider
private SpawnReplacementChests(model: string): CBaseAnimating {
const item = SpawnEntityFromTableSynchronous('prop_dynamic', {
model: model,
scale: this.GetScale(),
origin: this.position,
angles: this.angles,
skin: this.GetSkin(),
}) as CBaseAnimating;
item.ResetSequence('chest_treasure_idle');
return item as CBaseAnimating;
}
private RedropChest() {
const chestReplace = CreateItem(this.GetName(), undefined, undefined);
const item = CreateItemOnPositionSync(this.position, chestReplace);
item.SetAngles(this.angles.x, this.angles.y, this.angles.z);
item.SetModelScale(this.GetScale());
item.ResetSequence('chest_treasure_idle');
}
private DeletePlaceholder() {
this.channelingPlaceholder.RemoveSelf();
}
private DeleteChest() {
// Removing an item also destroys the underlying entity, be careful
this.GetCaster().RemoveItem(this);
this.DeletePlaceholder();
}
private CreateLoot() {
const caster = this.GetCaster();
const item = CreateItem('item_desolator', caster.GetPlayerOwner(), undefined);
const worldItem = CreateItemOnPositionSync(this.position, item);
item?.LaunchLoot(
false,
124,
RandomFloat(0.5, 1.2),
caster.GetOrigin().__add(RandomVector(RandomInt(50, 150))),
);
}
private GetChestOpenModel() {
return this.kv['ReplaceOnOpen'];
}
private GetChestModel() {
return this.kv['Model'];
}
private GetSkin() {
return tonumber(this.kv['Skin']) || 0;
}
protected GetScale() {
return tonumber(this.kv['Scale']) || 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
Finally you will want to give your chest a tooltip name. Add "DOTA_Tooltip_ability_item_treasure_chest_2" "Testchest" to your addon_*.txt.
There are some drawbacks to this approach. You can't change the pickup range and the item quality.