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 }