1 /*
2  * Copyright (c) 2017-2019 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: Copyright (c) 2017-2019 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/selery/source/selery/entity/living.d, selery/entity/living.d)
28  */
29 module selery.entity.living;
30 
31 import std.conv : to;
32 import std.math : round, isNaN;
33 
34 import sel.format : Format;
35 
36 import selery.about;
37 import selery.command.util : Position;
38 import selery.effect : Effect, Effects;
39 import selery.entity.entity : Entity, Rotation;
40 import selery.entity.metadata;
41 import selery.event.world;
42 import selery.math.vector;
43 import selery.player.player : Player;
44 import selery.util.color : Color;
45 import selery.util.util : safe, call;
46 import selery.world.world : World;
47 
48 static import sul.effects;
49 
50 public class Living : Entity {
51 
52 	protected Health m_health;
53 	protected Effect[ubyte] effects;
54 
55 	public bool immortal = false;
56 
57 	protected tick_t last_received_attack = 0;
58 	protected tick_t last_void_damage = 0;
59 
60 	private float n_speed = .1;
61 	private float n_base_speed = .1;
62 
63 	private tick_t despawn_after = 0;
64 
65 	public this(World world, EntityPosition position, uint health, uint max) {
66 		super(world, position);
67 		this.m_health = Health(health, max);
68 		this.metadata.set!"canClimb"(true);
69 	}
70 
71 	public override void tick() {
72 		super.tick();
73 		//void
74 		if(this.position.y < -4 && this.last_void_damage + 10 < this.ticks) {
75 			this.last_void_damage = this.ticks;
76 			if(this.last_puncher is null) {
77 				this.attack(new EntityDamageByVoidEvent(this));
78 			} else {
79 				this.attack(new EntityPushedIntoVoidEvent(this, this.last_puncher));
80 			}
81 		}
82 		//update the effects
83 		foreach(effect ; this.effects) {
84 			effect.tick();
85 			if(effect.finished) {
86 				this.removeEffect(effect);
87 			}
88 		}
89 		if(this.dead && this.despawn_after > 0 && --this.despawn_after == 0) {
90 			this.despawn();
91 		}
92 		if(this.moved) {
93 			this.updateGroundStatus();
94 		}
95 	}
96 
97 	public final pure nothrow @property @safe @nogc float speed() {
98 		return this.n_speed;
99 	}
100 
101 	public final nothrow @property @safe uint health() {
102 		return this.healthNoAbs + this.absorption;
103 	}
104 
105 	public final @property @safe uint health(uint health) {
106 		//TODO call events
107 		this.m_health.health = health;
108 		this.healthUpdated();
109 		return this.healthNoAbs;
110 	}
111 
112 	public final @property @safe @nogc uint maxHealth() {
113 		return this.maxHealthNoAbs + this.maxAbsorption;
114 	}
115 
116 	public final @property @safe uint maxHealth(uint max) {
117 		this.m_health.max = max;
118 		this.healthUpdated();
119 		return this.maxHealth;
120 	}
121 
122 	public final nothrow @property @safe uint healthNoAbs() {
123 		return this.m_health.health;
124 	}
125 
126 	public final pure nothrow @property @safe @nogc uint maxHealthNoAbs() {
127 		return this.m_health.max;
128 	}
129 
130 	public final nothrow @property @safe uint absorption() {
131 		return this.m_health.absorption;
132 	}
133 
134 	public final pure nothrow @property @safe @nogc uint maxAbsorption() {
135 		return this.m_health.maxAbsorption;
136 	}
137 
138 	protected @trusted void healthUpdated() {}
139 
140 	protected override bool validateAttack(EntityDamageEvent event) {
141 		//TODO the attack is applied if the damage is higher than the last one
142 		return this.alive && (!this.immortal || event.imminent) && (!cast(EntityDamageByEntityEvent)event || this.last_received_attack + 10 <= this.ticks);
143 	}
144 
145 	protected override void attackImpl(EntityDamageEvent event) {
146 		this.last_received_attack = this.ticks;
147 
148 		//update the health
149 		uint abb = this.absorption;
150 		this.m_health.remove(event.damage);
151 		if(abb > 0 && this.absorption == 0) {
152 			this.removeEffect(Effects.absorption);
153 		}
154 		this.healthUpdated();
155 
156 		//hurt animation
157 		this.viewers!Player.call!"sendHurtAnimation"(this);
158 
159 		//update the viewers if dead
160 		if(this.dead) {
161 			auto death = this.callDeathEvent(event);
162 			if(death.message.translatable.default_.length) {
163 				this.world.broadcast(Format.yellow, death.message);
164 			}
165 			this.die();
166 		} else if(cast(EntityAttackedByEntityEvent)event) {
167 			auto casted = cast(EntityDamageByEntityEvent)event;
168 			if(casted.doKnockback) {
169 				//TODO use knockback method?
170 				this.motion = casted.knockback;
171 			}
172 			this.last_puncher = casted.damager;
173 		}
174 	}
175 
176 	protected EntityDeathEvent callDeathEvent(EntityDamageEvent last) {
177 		auto event = new EntityDeathEvent(this, last);
178 		this.world.callEvent(event);
179 		return event;
180 	}
181 
182 	public @trusted void heal(EntityHealEvent event) {
183 		this.world.callEvent(event);
184 		if(!event.cancelled) {
185 			this.m_health.add(event.amount);
186 			this.healthUpdated();
187 		}
188 	}
189 
190 	public final override @property @safe bool alive() {
191 		return this.m_health.alive;
192 	}
193 
194 	public final override @property @safe bool dead() {
195 		return this.m_health.dead;
196 	}
197 
198 	/**
199 	 * Die and send the packets to the viewers
200 	 */
201 	protected void die() {
202 		this.viewers!Player.call!"sendDeathAnimation"(this);
203 		if((this.despawn_after = this.despawnAfter) == 0) {
204 			this.despawn();
205 		}
206 	}
207 
208 	protected @property @safe @nogc tick_t despawnAfter() {
209 		return 30;
210 	}
211 
212 	/**
213 	 * Adds an effect to the entity.
214 	 */
215 	public bool addEffect(sul.effects.Effect effect, ubyte level=0, tick_t duration=30, Living thrower=null) {
216 		return this.addEffect(Effect.fromId(effect, this, level, duration, thrower));
217 	}
218 
219 	public bool addEffect(Effect effect) {
220 		if(effect.instant) {
221 			effect.onStart();
222 		} else {
223 
224 			/+if(effect.id == Effects.healing.id || effect.id == Effects.harming.id) {
225 				//TODO for undead mobs
226 				if(effect.id == Effects.harming.id) {
227 					uint amount = to!uint(round(3 * effect.levelFromOne * multiplier));
228 					//this.attack(effect.thrower is null ? new EntityDamageEvent(this, Damage.MAGIC, amount) : new EntityDamagedByEntityEvent(this, Damage.MAGIC, amount, effect.thrower));
229 				}
230 				else this.heal(new EntityHealEvent(this, Healing.MAGIC, to!uint(round(3 * effect.levelFromOne * multiplier))));
231 				return true;
232 			}+/
233 
234 			if(effect.id in this.effects) this.removeEffect(effect); //TODO just edit instead of removing
235 
236 			this.effects[effect.id] = effect;
237 			effect.onStart();
238 
239 			/*if(effect.id == Effects.healthBoost) {
240 				this.m_health.max = 20 + effect.levelFromOne * 4;
241 				this.healthUpdated();
242 			} else if(effect.id == Effects.absorption) {
243 				this.m_health.maxAbsorption = effect.levelFromOne * 4;
244 			}*/
245 
246 			this.recalculateColors();
247 			this.onEffectAdded(effect, false);
248 		}
249 		return true;
250 	}
251 
252 	protected void onEffectAdded(Effect effect, bool modified) {}
253 
254 	/**
255 	 * Gets a pointer to an effect.
256 	 */
257 	public Effect* opBinaryRight(string op : "in")(ubyte id) {
258 		return id in this.effects;
259 	}
260 
261 	/// ditto
262 	public Effect* opBinaryRight(string op : "in")(inout sul.effects.Effect effect) {
263 		return this.opBinaryRight!"in"(effect.java.id);
264 	}
265 
266 	/**
267 	 * Removes an effect from the entity.
268 	 * Returns: whether the effect has been removed
269 	 */
270 	public bool removeEffect(sul.effects.Effect effect) {
271 		auto e = effect.java.id in this.effects;
272 		if(e) {
273 			this.effects.remove(effect.java.id);
274 			this.recalculateColors();
275 			(*e).onStop();
276 			/*if(effect.id == Effects.healthBoost) {
277 				this.m_health.max = 20;
278 				this.healthUpdated();
279 			} else if(effect.id == Effects.absorption) {
280 				this.m_health.maxAbsorption = 0;
281 			}*/
282 			this.onEffectRemoved(*e);
283 			return true;
284 		}
285 		return false;
286 	}
287 
288 	/// ditto
289 	public bool removeEffect(ubyte effect) {
290 		return (effect in this.effects) ? this.removeEffect(this.effects[effect]) : false;
291 	}
292 
293 	protected void onEffectRemoved(Effect effect) {}
294 
295 	/**
296 	 * Removes every effect.
297 	 * Returns: whether one or more effect has been removed
298 	 */
299 	public bool clearEffects() {
300 		bool ret = false;
301 		foreach(effect ; this.effects) {
302 			ret |= this.removeEffect(effect);
303 		}
304 		return ret;
305 	}
306 
307 	protected void recalculateColors() {
308 		if(this.effects.length > 0) {
309 			Color[] colors;
310 			foreach(effect ; this.effects) {
311 				foreach(uint i ; 0..effect.levelFromOne) {
312 					colors ~= Color.fromRGB(effect.particles);
313 				}
314 			}
315 			this.potionColor = new Color(colors);
316 			this.potionAmbient = true;
317 		} else {
318 			this.potionColor = null;
319 			this.potionAmbient = false;
320 		}
321 	}
322 
323 	public void recalculateSpeed() {
324 		float s = this.n_base_speed;
325 		auto speed = Effects.speed in this;
326 		auto slowness = Effects.slowness in this;
327 		if(speed) {
328 			s *= 1 + .2 * (*speed).levelFromOne;
329 		}
330 		if(slowness) {
331 			s /= 1 + .15 * (*slowness).levelFromOne;
332 		}
333 		if(this.sprinting) {
334 			s *= 1.3;
335 		}
336 		this.n_speed = s < 0 ? 0 : s;
337 	}
338 
339 	protected @property @trusted Color potionColor(Color color) {
340 		if(color is null) {
341 			this.metadata.set!"potionColor"(0);
342 		} else {
343 			auto c = color.rgb & 0xFFFFFF;
344 			foreach(p ; SupportedJavaProtocols) {
345 				mixin("this.metadata.java" ~ p.to!string ~ ".potionColor = c;");
346 			}
347 			c |= 0xFF000000;
348 			foreach(p ; SupportedBedrockProtocols) {
349 				mixin("this.metadata.bedrock" ~ p.to!string ~ ".potionColor = c;");
350 			}
351 		}
352 		return color;
353 	}
354 
355 	protected @property @trusted bool potionAmbient(bool flag) {
356 		this.metadata.set!"potionAmbient"(flag);
357 		return flag;
358 	}
359 
360 }
361 
362 struct Health {
363 
364 	public float m_health;
365 	public uint m_max;
366 
367 	public float m_absorption;
368 	public uint m_max_absorption;
369 
370 	public @safe this(uint health, uint max) {
371 		this.m_health = 0;
372 		this.m_absorption = 0;
373 		this.max = max;
374 		this.health = health;
375 		this.maxAbsorption = 0;
376 	}
377 
378 	public nothrow @property @safe @nogc uint health() {
379 		if(this.dead) return 0;
380 		uint ret = cast(uint)round(this.m_health);
381 		return ret == 0 ? 1 : ret;
382 	}
383 
384 	public @property @safe uint health(float health) {
385 		this.m_health = health;
386 		if(this.m_health > this.m_max) this.m_health = this.m_max;
387 		else if(this.m_health < 0) this.m_health = 0;
388 		return this.health;
389 	}
390 
391 	public pure nothrow @property @safe @nogc uint max() {
392 		return this.m_max;
393 	}
394 
395 	public @property @safe uint max(uint max) {
396 		this.m_max = max;
397 		this.health = this.health;
398 		return this.m_max;
399 	}
400 
401 	public nothrow @property @safe uint absorption() {
402 		return cast(uint)round(this.m_absorption);
403 	}
404 
405 	public pure nothrow @property @safe @nogc uint maxAbsorption() {
406 		return this.m_max_absorption;
407 	}
408 
409 	public @property @safe uint maxAbsorption(uint ma) {
410 		this.m_max_absorption = ma;
411 		this.m_absorption = ma;
412 		return this.maxAbsorption;
413 	}
414 
415 	public @safe void add(float amount) {
416 		this.health = this.m_health + amount;
417 	}
418 
419 	public @safe void remove(float amount) {
420 		if(this.m_absorption != 0) {
421 			if(amount <= this.m_absorption) {
422 				this.m_absorption -= amount;
423 				amount = 0;
424 			} else {
425 				amount -= this.m_absorption;
426 				this.m_absorption = 0;
427 			}
428 		}
429 		this.health = this.m_health - amount;
430 	}
431 
432 	public pure nothrow @property @safe @nogc bool alive() {
433 		return this.m_health != 0;
434 	}
435 
436 	public pure nothrow @property @safe @nogc bool dead() {
437 		return this.m_health == 0;
438 	}
439 
440 	public @safe void reset() {
441 		this.m_health = 20;
442 		this.m_max = 20;
443 		this.m_absorption = 0;
444 		this.m_max_absorption = 0;
445 	}
446 
447 }
448 
449 enum Healing : ubyte {
450 
451 	UNKNOWN = 0,
452 
453 	MAGIC = 1,
454 	NATURAL_REGENERATION = 2,
455 	REGENERATION = 3,
456 
457 }