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 }