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