package electrosphere.entity.state.hitbox; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.joml.Quaterniond; import org.joml.Vector3d; import org.joml.Vector3f; import org.ode4j.ode.DBody; import org.ode4j.ode.DGeom; import electrosphere.collision.CollisionBodyCreation; import electrosphere.collision.CollisionEngine; import electrosphere.collision.PhysicsEntityUtils; import electrosphere.collision.PhysicsUtils; import electrosphere.collision.collidable.Collidable; import electrosphere.collision.hitbox.HitboxManager; import electrosphere.collision.hitbox.HitboxUtils.HitboxPositionCallback; import electrosphere.entity.Entity; import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityUtils; import electrosphere.entity.state.hitbox.HitboxCollectionState.HitboxState.HitboxShapeType; import electrosphere.entity.types.attach.AttachUtils; import electrosphere.game.data.collidable.HitboxData; import electrosphere.game.data.utils.DataFormatUtil; import electrosphere.logger.LoggerInterface; import electrosphere.util.math.MathUtils; /** * The state of the collection of all hitboxes on this entity * Ie, it stores the state of each hitbox that is attached to this entity */ public class HitboxCollectionState { /** * Types of hitboxes */ public enum HitboxType { HIT, // damages another entity HURT, // receives damage from another entity BLOCK, // blocks a hit from another entity } /** * The subtype of hitbox */ public enum HitboxSubtype { SWEET, //extra damage REGULAR, //regular damage SOUR, //less damage } //the parent entity of the hitbox state Entity parent; //the body that contains all the hitbox shapes DBody body; //The collidable associated with the body Collidable collidable; //the list of all geoms in the collection state List geoms = new LinkedList(); //the map of bone -> hitbox shape in ode4j Map hitboxGeomMap = new HashMap(); //the map of geometry -> hitbox shape status, useful for finding data about a given hitbox during collision Map geomStateMap = new HashMap(); //callback to provide a position for the hitbox each frame HitboxPositionCallback positionCallback; //controls whether the hitbox state is active or not boolean active = true; //controls whether active hitboxes should be overwritten with block boxes boolean blockOverride = false; //the associated manager HitboxManager manager; //controls whether this hitbox collection thinks its on the server or client boolean isServer = true; /** * Create hitbox state for an entity * @param collisionEngine the collision engine * @param entity The entity to attach the state to * @param hitboxListRaw The list of hitbox data to apply * @return The hitbox state that has been attached to the entity */ public static HitboxCollectionState attachHitboxState(HitboxManager manager, boolean isServer, Entity entity, List hitboxListRaw){ HitboxCollectionState rVal = new HitboxCollectionState(); rVal.isServer = isServer; //create the shapes for(HitboxData hitboxDataRaw : hitboxListRaw){ DGeom geom = null; HitboxType type = HitboxType.HIT; HitboxShapeType shapeType = HitboxShapeType.SPHERE; // //Get the type as an enum // switch(hitboxDataRaw.getType()){ case HitboxData.HITBOX_TYPE_HIT: { type = HitboxType.HIT; shapeType = HitboxShapeType.SPHERE; geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT); } break; case HitboxData.HITBOX_TYPE_HURT: { type = HitboxType.HURT; shapeType = HitboxShapeType.SPHERE; geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT); } break; case HitboxData.HITBOX_TYPE_HIT_CONNECTED: { type = HitboxType.HIT; shapeType = HitboxShapeType.CAPSULE; geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT); } break; case HitboxData.HITBOX_TYPE_HURT_CONNECTED: { type = HitboxType.HURT; shapeType = HitboxShapeType.CAPSULE; geom = CollisionBodyCreation.createShapeSphere(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), Collidable.TYPE_OBJECT_BIT); } break; case HitboxData.HITBOX_TYPE_STATIC_CAPSULE: { type = HitboxType.HURT; shapeType = HitboxShapeType.STATIC_CAPSULE; geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxDataRaw.getRadius(), hitboxDataRaw.getLength(), Collidable.TYPE_OBJECT_BIT); } break; } // //Get the subtype as an enum // HitboxSubtype subType; String subTypeRaw = hitboxDataRaw.getSubType(); if(subTypeRaw == null){ subTypeRaw = HitboxData.HITBOX_SUBTYPE_REUGLAR; } switch(subTypeRaw){ case HitboxData.HITBOX_SUBTYPE_SWEET: { subType = HitboxSubtype.SWEET; } break; case HitboxData.HITBOX_SUBTYPE_REUGLAR: { subType = HitboxSubtype.REGULAR; } break; case HitboxData.HITBOX_SUBTYPE_SOUR: { subType = HitboxSubtype.SOUR; } break; default: { subType = HitboxSubtype.REGULAR; } break; } if(hitboxDataRaw.getBone() != null){ rVal.hitboxGeomMap.put(hitboxDataRaw.getBone(),geom); } rVal.geoms.add(geom); rVal.geomStateMap.put(geom,new HitboxState(hitboxDataRaw.getBone(), hitboxDataRaw, type, subType, shapeType, false)); } //create body with all the shapes DGeom[] geomArray = rVal.geoms.toArray(new DGeom[rVal.geoms.size()]); rVal.body = CollisionBodyCreation.createBodyWithShapes(manager.getCollisionEngine(), geomArray); //register collidable with collision engine Collidable collidable = new Collidable(entity, Collidable.TYPE_OBJECT); manager.getCollisionEngine().registerCollisionObject(rVal.body, collidable); //attach entity.putData(EntityDataStrings.HITBOX_DATA, rVal); rVal.parent = entity; //register manager.registerHitbox(rVal); rVal.manager = manager; return rVal; } /** * Create hitbox state for an entity * @param collisionEngine the collision engine * @param entity The entity to attach the state to * @param data The hitbox data to apply * @param callback The callback that provides a position for the hitbox each frame * @return The hitbox state that has been attached to the entity */ public static HitboxCollectionState attachHitboxStateWithCallback(HitboxManager manager, CollisionEngine collisionEngine, Entity entity, HitboxData data, HitboxPositionCallback callback){ HitboxCollectionState rVal = new HitboxCollectionState(); //create the shapes rVal.hitboxGeomMap.put(data.getBone(),CollisionBodyCreation.createShapeSphere(collisionEngine, data.getRadius(), Collidable.TYPE_OBJECT_BIT)); //create body with all the shapes DGeom[] geomArray = rVal.hitboxGeomMap.values().toArray(new DGeom[rVal.hitboxGeomMap.values().size()]); rVal.body = CollisionBodyCreation.createBodyWithShapes(collisionEngine, geomArray); //register collidable with collision engine Collidable collidable = new Collidable(entity, Collidable.TYPE_OBJECT); collisionEngine.registerCollisionObject(rVal.body, collidable); //attach entity.putData(EntityDataStrings.HITBOX_DATA, rVal); rVal.parent = entity; //register manager.registerHitbox(rVal); rVal.manager = manager; return rVal; } /** * Clears the collision status of all shapes */ public void clearCollisions(){ for(DGeom geom : this.geoms){ HitboxState shapeStatus = this.geomStateMap.get(geom); shapeStatus.setHadCollision(false); } } /** * Updates the positions of all hitboxes */ public void updateHitboxPositions(CollisionEngine collisionEngine){ if(parent != null && !isServer && EntityUtils.getActor(parent) != null){ if(!this.hitboxGeomMap.isEmpty()){ Vector3d entityPosition = EntityUtils.getPosition(parent); this.body.setPosition(PhysicsUtils.jomlVecToOdeVec(entityPosition)); for(String boneName : this.hitboxGeomMap.keySet()){ Vector3f bonePosition = EntityUtils.getActor(parent).getBonePosition(boneName); DGeom geom = this.hitboxGeomMap.get(boneName); HitboxState shapeStatus = this.geomStateMap.get(geom); switch(shapeStatus.shapeType){ case SPHERE: { this.updateSphereShapePosition(collisionEngine,boneName,shapeStatus,bonePosition); } break; case CAPSULE: { this.updateCapsuleShapePosition(collisionEngine,boneName,shapeStatus,bonePosition); } break; case STATIC_CAPSULE: { } break; } } } else if(positionCallback != null){ DGeom geom = body.getGeomIterator().next(); Vector3d worldPosition = this.positionCallback.getPosition(); Quaterniond rotation = new Quaterniond().identity(); PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, worldPosition, rotation); } } else if(parent != null && isServer && EntityUtils.getPoseActor(parent) != null){ if(!this.hitboxGeomMap.isEmpty()){ Vector3d entityPosition = EntityUtils.getPosition(parent); this.body.setPosition(PhysicsUtils.jomlVecToOdeVec(entityPosition)); for(String boneName : this.hitboxGeomMap.keySet()){ Vector3f bonePosition = EntityUtils.getPoseActor(parent).getBonePosition(boneName); DGeom geom = this.hitboxGeomMap.get(boneName); HitboxState shapeStatus = this.geomStateMap.get(geom); switch(shapeStatus.shapeType){ case SPHERE: { this.updateSphereShapePosition(collisionEngine,boneName,shapeStatus,bonePosition); } break; case CAPSULE: { this.updateCapsuleShapePosition(collisionEngine,boneName,shapeStatus,bonePosition); } break; case STATIC_CAPSULE: { } break; } } } else if(positionCallback != null){ DGeom geom = body.getGeomIterator().next(); Vector3d worldPosition = this.positionCallback.getPosition(); Quaterniond rotation = new Quaterniond().identity(); PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, worldPosition, rotation); } } else if(parent != null && isServer){ for(DGeom geom : this.geoms){ HitboxState shapeStatus = this.geomStateMap.get(geom); switch(shapeStatus.shapeType){ case SPHERE: { } break; case CAPSULE: { } break; case STATIC_CAPSULE: { this.updateStaticCapsulePosition(collisionEngine, geom, shapeStatus); } break; } } } } /** * Updates the position of the geom for a static capsule * @param collisionEngine The collision engine * @param boneName The name of the bone the static capsule is attached to * @param bonePosition The position of the bone */ private void updateStaticCapsulePosition(CollisionEngine collisionEngine, DGeom geom, HitboxState shapeStatus){ Vector3d parentPos = EntityUtils.getPosition(parent); PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, parentPos, new Quaterniond(0.707,0,0,0.707)); } /** * Updates the position of a sphere-shape-type hitbox * @param collisionEngine The collision engine * @param boneName The name of the bone * @param bonePosition the position of the bone */ private void updateSphereShapePosition(CollisionEngine collisionEngine, String boneName, HitboxState hitboxState, Vector3f bonePosition){ DGeom geom = this.hitboxGeomMap.get(boneName); //get offset's transform Vector3d offsetPosition = DataFormatUtil.getDoubleListAsVector(hitboxState.getHitboxData().getOffset()); Quaterniond offsetRotation = new Quaterniond(); //the bone's transform Vector3d bonePositionD = new Vector3d(bonePosition); Quaterniond boneRotation = new Quaterniond(); //the parent's transform Vector3d parentPosition = EntityUtils.getPosition(parent); Quaterniond parentRotation = EntityUtils.getRotation(parent); Vector3d parentScale = new Vector3d(EntityUtils.getScale(parent)); //calculate Vector3d hitboxPos = AttachUtils.calculateBoneAttachmentPosition(offsetPosition, offsetRotation, bonePositionD, boneRotation, parentPosition, parentRotation, parentScale); PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, hitboxPos, new Quaterniond()); } /** * Updates the position of a capsule-shape hitbox * @param collisionEngine * @param boneName * @param bonePosition */ private void updateCapsuleShapePosition(CollisionEngine collisionEngine, String boneName, HitboxState hitboxState, Vector3f bonePosition){ //get data about the hitbox DGeom geom = this.hitboxGeomMap.get(boneName); Vector3d previousWorldPos = hitboxState.getPreviousWorldPos(); double length = hitboxState.getHitboxData().getRadius(); //get offset's transform Vector3d offsetPosition = DataFormatUtil.getDoubleListAsVector(hitboxState.getHitboxData().getOffset()); Quaterniond offsetRotation = new Quaterniond(); //the bone's transform Vector3d bonePositionD = new Vector3d(bonePosition); Quaterniond boneRotation = new Quaterniond(); //the parent's transform Vector3d parentPosition = EntityUtils.getPosition(parent); Quaterniond parentRotation = EntityUtils.getRotation(parent); Vector3d parentScale = new Vector3d(EntityUtils.getScale(parent)); //calculate Vector3d worldPosition = AttachUtils.calculateBoneAttachmentPosition(offsetPosition, offsetRotation, bonePositionD, boneRotation, parentPosition, parentRotation, parentScale); Quaterniond worldRotation = new Quaterniond(); if(previousWorldPos != null){ //called all subsequent updates to hitbox position //destroy old capsule this.geomStateMap.remove(geom); this.geoms.remove(geom); CollisionBodyCreation.destroyShape(collisionEngine, geom); //calculate position between new world point and old world point Vector3d bodyPosition = new Vector3d(worldPosition).lerp(previousWorldPos, 0.5); //calculate rotation from old position to new position //the second quaternion is a rotation along the x axis. This is used to put the hitbox rotation into ode's space //ode is Z-axis-up if(previousWorldPos.distance(worldPosition) > 0.0){ worldRotation = MathUtils.calculateRotationFromPointToPoint(previousWorldPos,worldPosition).mul(new Quaterniond(0,0.707,0,0.707)); } //create new capsule length = previousWorldPos.distance(worldPosition) / 2.0; if(length > 5000 || Double.isNaN(length) || Double.isInfinite(length) || length < 0){ if(length < 0){ LoggerInterface.loggerEngine.WARNING("Length is too short! " + length); } if(length > 5000){ LoggerInterface.loggerEngine.WARNING("Length is too long! " + length); } if(Double.isNaN(length) || Double.isInfinite(length)){ LoggerInterface.loggerEngine.WARNING("Length is invalid number! " + length); } if(Double.isNaN(previousWorldPos.x) || Double.isInfinite(previousWorldPos.x)){ LoggerInterface.loggerEngine.WARNING("Previous hitbox position isn't valid!"); } if(Double.isNaN(worldPosition.x) || Double.isInfinite(worldPosition.x)){ LoggerInterface.loggerEngine.WARNING("Current hitbox position isn't valid!"); } length = 0.1; } geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxState.getHitboxData().getRadius(), length, Collidable.TYPE_OBJECT_BIT); CollisionBodyCreation.attachGeomToBody(collisionEngine,body,geom); PhysicsEntityUtils.setGeometryPosition(collisionEngine, geom, bodyPosition, worldRotation); } else { //called first time the hitbox updates position this.geomStateMap.remove(geom); this.geoms.remove(geom); CollisionBodyCreation.destroyShape(collisionEngine, geom); //create new capsule geom = CollisionBodyCreation.createCapsuleShape(manager.getCollisionEngine(), hitboxState.getHitboxData().getRadius(), length, Collidable.TYPE_OBJECT_BIT); CollisionBodyCreation.attachGeomToBody(collisionEngine,body,geom); } //update maps and other variables for next frame this.hitboxGeomMap.put(boneName,geom); this.geomStateMap.put(geom,hitboxState); this.geoms.add(geom); hitboxState.setPreviousWorldPos(worldPosition); } /** * Gets the status of a shape in the hitbox object * @param geom The geometry that is the shape within the hitbox data * @return The status of the shape */ public HitboxState getShapeStatus(DGeom geom){ return this.geomStateMap.get(geom); } /** * Gets the hitbox state of the entity * @param entity the entity * @return the hitbox state if it exists */ public static HitboxCollectionState getHitboxState(Entity entity){ return (HitboxCollectionState)entity.getData(EntityDataStrings.HITBOX_DATA); } /** * Checks whether the entity has hitbox state or not * @param entity the entity to check * @return true if there is hitbox state, false otherwise */ public static boolean hasHitboxState(Entity entity){ return entity.containsKey(EntityDataStrings.HITBOX_DATA); } /** * Destroys the hitbox state and removes it from the entity * @param entity the entity * @return The hitbox state if it exists, null otherwise */ public static HitboxCollectionState destroyHitboxState(Entity entity){ HitboxCollectionState state = null; if(hasHitboxState(entity)){ state = getHitboxState(entity); state.manager.deregisterHitbox(state); } return state; } /** * Gets whether the hitbox state is active or not * @return true if active, false otherwise */ public boolean isActive(){ return active; } /** * Sets the active state of the hitbox * @param state true to make it active, false otherwise */ public void setActive(boolean state){ this.active = state; for(DGeom geom : this.geoms){ HitboxState shapeState = this.getShapeStatus(geom); shapeState.setActive(state); } } /** * Gets the block override status * @return true if should override hitboxes with blockboxes, false otherwise */ public boolean isBlockOverride(){ return this.blockOverride; } /** * Sets the block override of the hitbox * @param state true to override attack hitboxes with block boxes, false otherwise */ public void setBlockOverride(boolean state){ this.blockOverride = state; for(DGeom geom : this.geoms){ HitboxState shapeState = this.getShapeStatus(geom); shapeState.setBlockOverride(state); } } /** * Gets the list of all DGeoms in the data * @return the list of all DGeoms */ public List getGeometries(){ return this.geoms; } /** * Gets the set of bone names in the state data * @return The set of bone names in the state data */ public Set getBones(){ return this.hitboxGeomMap.keySet(); } /** * Gets geometry on a single hitbox based on its bone name * @param boneName the bone name * @return the hitbox geometry */ public DGeom getGeometry(String boneName){ return this.hitboxGeomMap.get(boneName); } /** * The status of a single shape inside the overall hitbox data * IE a single sphere on the overall body */ public static class HitboxState { /** * Types of geometry that can be used as individual shapes within a hitbox */ public enum HitboxShapeType { //this is a true sphere. It will teleport every frame to its new position SPHERE, //for this one, the shape is a capsule in the collision engine, however //the capsule is used to have continuity between the last position the hitbox occupied and the current one CAPSULE, //this is a true static capsule, it doesn't act as two connected spheres but is instead a capsule that teleports between frames STATIC_CAPSULE, } //the name of the bone the hitbox is attached to String boneName; //the type of hitbox HitboxType type; //the subtype HitboxSubtype subType; //the type of geometry HitboxShapeType shapeType; //controls whether the hitbox is active boolean isActive; //controls whether the block override is active or not //if it is active, hitboxes should behave like block boxes boolean blockOverride = false; //the previous position of this hitbox shape Vector3d previousWorldPos = null; //if true, just had a collision boolean hadCollision = false; //the data of the hitbox HitboxData data; /** * Creates a status object for a hitbox * @param boneName The name of the bone the hitbox is attached to, if any * @param data the hitbox data object * @param type The type of hitbox * @param subType The subtype of hitbox * @param shapeType The type of shape the hitbox is * @param isActive if the hitbox is active or not */ public HitboxState(String boneName, HitboxData data, HitboxType type, HitboxSubtype subType, HitboxShapeType shapeType, boolean isActive){ this.boneName = boneName; this.data = data; this.type = type; this.subType = subType; this.shapeType = shapeType; this.isActive = isActive; } /** * Gets the name of the bone the hitbox is attached to * @return The name of the bone */ public String getBoneName(){ return boneName; } /** * Sets the name of the bone the hitbox is attached to * @param boneName The bone name */ public void setBoneName(String boneName){ this.boneName = boneName; } /** * Gets the hitbox data for this shape * @return The data */ public HitboxData getHitboxData(){ return this.data; } /** * Sets the hitbox data for this shape * @param data The data */ public void setHitboxData(HitboxData data){ this.data = data; } /** * Gets the type of hitbox * @return The type */ public HitboxType getType(){ return type; } /** * Sets the type of hitbox * @param type The type */ public void setType(HitboxType type){ this.type = type; } /** * Gets the subtype of the hitbox * @return The subtype */ public HitboxSubtype getSubType(){ return subType; } /** * Sets the subtype of the hitbox * @param subType The subtype */ public void setSubType(HitboxSubtype subType){ this.subType = subType; } /** * Gets whether the hitbox is active or not * @return true if active, false otherwise */ public boolean isActive(){ return isActive; } /** * Sets whether the hitbox is active or not * @param active true for active, false otherwise */ public void setActive(boolean active){ this.isActive = active; } /** * Gets whether the block override is active or not * @return true if the block override is active, false otherwise */ public boolean isBlockOverride(){ return blockOverride; } /** * Sets whether the block override is active or not * @param blockOverride true if the block override is active, false otherwise */ public void setBlockOverride(boolean blockOverride){ this.blockOverride = blockOverride; } /** * Gets the previous world position of this hitbox * @return The previous world position */ public Vector3d getPreviousWorldPos(){ return this.previousWorldPos; } /** * sets the previous world position of this hitbox shape * @param previousWorldPos The previous world position */ public void setPreviousWorldPos(Vector3d previousWorldPos){ this.previousWorldPos = previousWorldPos; } /** * Sets the status of whether this hitbox just had a collision or not * @param hadCollision true if had a collision, false otherwise */ public void setHadCollision(boolean hadCollision){ this.hadCollision = hadCollision; } /** * Gets the collision status of the hitbox * @return true if had a collision, false otherwise */ public boolean getHadCollision(){ return this.hadCollision; } } }