package com.flansmod.common.driveables; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ChatComponentText; import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.world.World; import net.minecraftforge.fml.common.FMLCommonHandler; import com.flansmod.common.FlansMod; import com.flansmod.common.network.PacketDriveableControl; import com.flansmod.common.network.PacketDriveableKey; import com.flansmod.common.network.PacketPlaneControl; import com.flansmod.common.network.PacketPlaySound; import com.flansmod.common.teams.TeamsManager; import com.flansmod.common.tools.ItemTool; import com.flansmod.common.vector.Matrix4f; import com.flansmod.common.vector.Vector3f; public class EntityPlane extends EntityDriveable { /** The flap positions, used for renderering and for controlling the plane rotations */ public float flapsYaw, flapsPitchLeft, flapsPitchRight; /** Position of looping engine sound */ public int soundPosition; /** The angle of the propeller for the renderer */ public float propAngle; /** Weapon delays */ public int bombDelay, gunDelay; /** Despawn timer */ public int ticksSinceUsed = 0; /** Mostly aesthetic model variables. Gear actually has a variable hitbox */ public boolean varGear = true, varDoor = false, varWing = false; /** Delayer for gear, door and wing buttons */ public int toggleTimer = 0; /** Current plane mode */ public EnumPlaneMode mode; public EntityPlane(World world) { super(world); } public EntityPlane(World world, double x, double y, double z, PlaneType type, DriveableData data) { super(world, type, data); setPosition(x, y, z); prevPosX = x; prevPosY = y; prevPosZ = z; initType(type, false); } public EntityPlane(World world, double x, double y, double z, EntityPlayer placer, PlaneType type, DriveableData data) { this(world, x, y, z, type, data); rotateYaw(placer.rotationYaw + 90F); rotatePitch(type.restingPitch); } @Override public void initType(DriveableType type, boolean clientSide) { super.initType(type, clientSide); mode = (((PlaneType)type).mode == EnumPlaneMode.HELI ? EnumPlaneMode.HELI : EnumPlaneMode.PLANE); } @Override protected void writeEntityToNBT(NBTTagCompound tag) { super.writeEntityToNBT(tag); tag.setTag("Pos", this.newDoubleNBTList(this.posX, this.posY + 1D, this.posZ)); tag.setBoolean("VarGear", varGear); tag.setBoolean("VarDoor", varDoor); tag.setBoolean("VarWing", varWing); } @Override protected void readEntityFromNBT(NBTTagCompound tag) { super.readEntityFromNBT(tag); varGear = tag.getBoolean("VarGear"); varDoor = tag.getBoolean("VarDoor"); varWing = tag.getBoolean("VarWing"); } /** * Called with the movement of the mouse. Used in controlling vehicles if need be. * @param deltaY * @param deltaX */ @Override public void onMouseMoved(int deltaX, int deltaY) { if(!FMLCommonHandler.instance().getSide().isClient()) return; if(!FlansMod.proxy.mouseControlEnabled()) return; float sensitivity = 0.02F; flapsPitchLeft -= sensitivity * deltaY; flapsPitchRight -= sensitivity * deltaY; flapsPitchLeft -= sensitivity * deltaX; flapsPitchRight += sensitivity * deltaX; } @Override public void setPositionRotationAndMotion(double x, double y, double z, float yaw, float pitch, float roll, double motX, double motY, double motZ, float velYaw, float velPitch, float velRoll, float throt, float steeringYaw) { super.setPositionRotationAndMotion(x, y, z, yaw, pitch, roll, motX, motY, motZ, velYaw, velPitch, velRoll, throt, steeringYaw); flapsYaw = steeringYaw; } @Override public boolean interactFirst(EntityPlayer entityplayer) { if(isDead) return false; if(worldObj.isRemote) return false; //If they are using a repair tool, don't put them in ItemStack currentItem = entityplayer.getCurrentEquippedItem(); if(currentItem != null && currentItem.getItem() instanceof ItemTool && ((ItemTool)currentItem.getItem()).type.healDriveables) return true; PlaneType type = this.getPlaneType(); //Check each seat in order to see if the player can sit in it for(int i = 0; i <= type.numPassengers; i++) { if(seats[i].interactFirst(entityplayer)) { if(i == 0) { bombDelay = type.planeBombDelay; FlansMod.proxy.doTutorialStuff(entityplayer, this); } return true; } } return false; } @Override public boolean pressKey(int key, EntityPlayer player) { PlaneType type = this.getPlaneType(); //Send keys which require server side updates to the server if(worldObj.isRemote && (key == 6 || key == 8 || key == 9)) { FlansMod.getPacketHandler().sendToServer(new PacketDriveableKey(key)); return true; } boolean canThrust = ((seats[0] != null && seats[0].riddenByEntity instanceof EntityPlayer && ((EntityPlayer)seats[0].riddenByEntity).capabilities.isCreativeMode) || getDriveableData().fuelInTank > 0) && hasWorkingProp(); switch(key) { case 0 : //Accelerate : Increase the throttle, up to 1. { if(canThrust || throttle < 0F) { throttle += 0.002F; if(throttle > 1F) throttle = 1F; } return true; } case 1 : //Decelerate : Decrease the throttle, down to -1, or 0 if the plane cannot reverse { if(canThrust || throttle > 0F) { throttle -= 0.005F; if(throttle < -1F) throttle = -1F; if(throttle < 0F && type.maxNegativeThrottle == 0F) throttle = 0F; } return true; } case 2 : //Left : Yaw the flaps left { flapsYaw -= 1F; return true; } case 3 : //Right : Yaw the flaps right { flapsYaw += 1F; return true; } case 4 : //Up : Pitch the flaps up { flapsPitchLeft += 1F; flapsPitchRight += 1F; return true; } case 5 : //Down : Pitch the flaps down { flapsPitchLeft -= 1F; flapsPitchRight -= 1F; return true; } case 6 : //Exit : Get out { if(seats[0].riddenByEntity != null) seats[0].riddenByEntity.mountEntity(null); return true; } case 7 : //Inventory : Check to see if this plane allows in-flight inventory editing or if the plane is on the ground { if(worldObj.isRemote && (type.invInflight || (Math.abs(throttle) < 0.1F && onGround))) { FlansMod.proxy.openDriveableMenu((EntityPlayer)seats[0].riddenByEntity, worldObj, this); } return true; } case 8 : //Drop bomb case 9 : //Shoot bullet { return super.pressKey(key, player); } case 10 : //Change control mode { FlansMod.proxy.changeControlMode((EntityPlayer)seats[0].riddenByEntity); return true; } case 11 : //Roll left { flapsPitchLeft += 1F; flapsPitchRight -= 1F; return true; } case 12 : //Roll right { flapsPitchLeft -= 1F; flapsPitchRight += 1F; return true; } case 13 : // Gear { if(toggleTimer <= 0) { varGear = !varGear; player.addChatMessage(new ChatComponentText("Landing gear " + (varGear ? "down" : "up"))); toggleTimer = 10; FlansMod.getPacketHandler().sendToServer(new PacketDriveableControl(this)); } return true; } case 14 : // Door { if(toggleTimer <= 0) { varDoor = !varDoor; if(type.hasDoor) player.addChatMessage(new ChatComponentText("Doors " + (varDoor ? "open" : "closed"))); toggleTimer = 10; FlansMod.getPacketHandler().sendToServer(new PacketDriveableControl(this)); } return true; } case 15 : // Wing { if(toggleTimer <= 0) { if(type.hasWing) { varWing = !varWing; player.addChatMessage(new ChatComponentText("Switching mode")); } if(type.mode == EnumPlaneMode.VTOL) { if(mode == EnumPlaneMode.HELI) mode = EnumPlaneMode.PLANE; else mode = EnumPlaneMode.HELI; player.addChatMessage(new ChatComponentText(mode == EnumPlaneMode.HELI ? "Entering hover mode" : "Entering plane mode")); } toggleTimer = 10; FlansMod.getPacketHandler().sendToServer(new PacketDriveableControl(this)); } return true; } case 16 : // Trim Button { axes.setAngles(axes.getYaw(), 0, 0); return true; } case 17 : //Park { break; } } return false; } @Override public void updateKeyHeldState(int key, boolean held) { super.updateKeyHeldState(key, held); //On the server. For semi-auto weapons, shoot! if(!worldObj.isRemote) { switch(key) { case 9 : //Left Mouse case 8 : //Right Mouse } } } @Override public void onUpdate() { super.onUpdate(); //Get plane type PlaneType type = getPlaneType(); DriveableData data = getDriveableData(); if(type == null) { FlansMod.log("Plane type null. Not ticking plane"); return; } //Work out if this is the client side and the player is driving boolean thePlayerIsDrivingThis = worldObj.isRemote && seats[0] != null && seats[0].riddenByEntity instanceof EntityPlayer && FlansMod.proxy.isThePlayer((EntityPlayer)seats[0].riddenByEntity); //Despawning ticksSinceUsed++; if(!worldObj.isRemote && seats[0].riddenByEntity != null) ticksSinceUsed = 0; if(!worldObj.isRemote && TeamsManager.planeLife > 0 && ticksSinceUsed > TeamsManager.planeLife * 20) { setDead(); } //Shooting, inventories, etc. //Decrement bomb and gun timers if(bombDelay > 0) bombDelay--; if(gunDelay > 0) gunDelay--; if(toggleTimer > 0) toggleTimer--; //Aesthetics //Rotate the propellers if(hasEnoughFuel()) { propAngle += (Math.pow(throttle, 0.4))*1.5; } //Return the flaps to their resting position flapsYaw *= 0.9F; flapsPitchLeft *= 0.9F; flapsPitchRight *= 0.9F; //Limit flap angles if(flapsYaw > 20) flapsYaw = 20; if(flapsYaw < -20) flapsYaw = -20; if(flapsPitchRight > 20) flapsPitchRight = 20; if(flapsPitchRight < -20) flapsPitchRight = -20; if(flapsPitchLeft > 20) flapsPitchLeft = 20; if(flapsPitchLeft < -20) flapsPitchLeft = -20; //Player is not driving this. Update its position from server update packets if(worldObj.isRemote && !thePlayerIsDrivingThis) { //The driveable is currently moving towards its server position. Continue doing so. if (serverPositionTransitionTicker > 0) { double x = posX + (serverPosX - posX) / serverPositionTransitionTicker; double y = posY + (serverPosY - posY) / serverPositionTransitionTicker; double z = posZ + (serverPosZ - posZ) / serverPositionTransitionTicker; double dYaw = MathHelper.wrapAngleTo180_double(serverYaw - axes.getYaw()); double dPitch = MathHelper.wrapAngleTo180_double(serverPitch - axes.getPitch()); double dRoll = MathHelper.wrapAngleTo180_double(serverRoll - axes.getRoll()); rotationYaw = (float)(axes.getYaw() + dYaw / serverPositionTransitionTicker); rotationPitch = (float)(axes.getPitch() + dPitch / serverPositionTransitionTicker); float rotationRoll = (float)(axes.getRoll() + dRoll / serverPositionTransitionTicker); --serverPositionTransitionTicker; setPosition(x, y, z); setRotation(rotationYaw, rotationPitch, rotationRoll); //return; } //If the driveable is at its server position and does not have the next update, it should just simulate itself as a server side plane would, so continue } //Movement //Throttle handling //Without a player, default to 0 //With a player default to 0.5 for helicopters (hover speed) //And default to the range 0.25 ~ 0.5 for planes (taxi speed ~ take off speed) float throttlePull = 0.99F; if(seats[0] != null && seats[0].riddenByEntity != null && mode == EnumPlaneMode.HELI && canThrust()) throttle = (throttle - 0.5F) * throttlePull + 0.5F; //Get the speed of the plane float lastTickSpeed = (float)getSpeedXYZ(); //Alter angles //Sensitivity function float sensitivityAdjust = (throttle > 0.5F ? 1.5F - throttle : 4F * throttle - 1F); if(sensitivityAdjust < 0F) sensitivityAdjust = 0F; //Scalar sensitivityAdjust *= 0.125F; float yaw = flapsYaw * (flapsYaw > 0 ? type.turnLeftModifier : type.turnRightModifier) * sensitivityAdjust; //if(throttle < 0.2F) // sensitivityAdjust = throttle * 2.5F; //Pitch according to the sum of flapsPitchLeft and flapsPitchRight / 2 float flapsPitch = (flapsPitchLeft + flapsPitchRight) / 2F; float pitch = flapsPitch * (flapsPitch > 0 ? type.lookUpModifier : type.lookDownModifier) * sensitivityAdjust; //Roll according to the difference between flapsPitchLeft and flapsPitchRight / 2 float flapsRoll = (flapsPitchRight - flapsPitchLeft) / 2F; float roll = flapsRoll * (flapsRoll > 0 ? type.rollLeftModifier : type.rollRightModifier) * sensitivityAdjust; //Damage modifiers if(mode == EnumPlaneMode.PLANE) { if(!isPartIntact(EnumDriveablePart.tail)) { yaw = 0; pitch = 0; roll = 0; } if(!isPartIntact(EnumDriveablePart.leftWing)) roll -= 7F * getSpeedXZ(); if(!isPartIntact(EnumDriveablePart.rightWing)) roll += 7F * getSpeedXZ(); } axes.rotateLocalYaw(yaw); axes.rotateLocalPitch(pitch); axes.rotateLocalRoll(-roll); if(worldObj.isRemote && !FlansMod.proxy.mouseControlEnabled()) { //axes.rotateGlobalRoll(-axes.getRoll() * 0.1F); } //Some constants float g = 0.98F / 10F; float drag = 1F - (0.05F * type.drag); float wobbleFactor = 0F;//.005F; float throttleScaled = 0.01F * (type.maxThrottle + (data.engine == null ? 0 : data.engine.engineSpeed)); if(!canThrust()) throttleScaled = 0; int numPropsWorking = 0; int numProps = 0; float fuelConsumptionMultiplier = 2F; switch(mode) { case HELI : //Count the number of working propellers for(Propeller prop : type.heliPropellers) if(isPartIntact(prop.planePart)) numPropsWorking++; numProps = type.heliPropellers.size(); Vector3f up = axes.getYAxis(); throttleScaled *= numProps == 0 ? 0 : numPropsWorking / numProps * 2F; float upwardsForce = throttle * throttleScaled + (g - throttleScaled / 2F); if(throttle < 0.5F) upwardsForce = g * throttle * 2F; if(!isPartIntact(EnumDriveablePart.blades)) { upwardsForce = 0F; } //Move up //Throttle - 0.5 means that the positive throttle scales from -0.5 to +0.5. Thus it accounts for gravity-ish motionX += upwardsForce * up.x * 0.5F; motionY += upwardsForce * up.y; motionZ += upwardsForce * up.z * 0.5F; //Apply gravity motionY -= g; //Apply wobble //motionX += rand.nextGaussian() * wobbleFactor; //motionY += rand.nextGaussian() * wobbleFactor; //motionZ += rand.nextGaussian() * wobbleFactor; //Apply drag motionX *= drag; motionY *= drag; motionZ *= drag; data.fuelInTank -= upwardsForce * fuelConsumptionMultiplier * data.engine.fuelConsumption; break; case PLANE : //Count the number of working propellers for(Propeller prop : type.propellers) if(isPartIntact(prop.planePart)) numPropsWorking++; numProps = type.propellers.size(); float throttleTemp = throttle * (numProps == 0 ? 0 : numPropsWorking / numProps * 2F); //Apply forces Vector3f forwards = (Vector3f)axes.getXAxis().normalise(); //Sanity limiter if(lastTickSpeed > 2F) lastTickSpeed = 2F; float newSpeed = lastTickSpeed + throttleScaled * 2F; //Calculate the amount to alter motion by float proportionOfMotionToCorrect = 2F * throttleTemp - 0.5F; if(proportionOfMotionToCorrect < 0F) proportionOfMotionToCorrect = 0F; if(proportionOfMotionToCorrect > 0.5F) proportionOfMotionToCorrect = 0.5F; //Apply gravity g = 0.98F / 20F; motionY -= g; //Apply lift int numWingsIntact = 0; if(isPartIntact(EnumDriveablePart.rightWing)) numWingsIntact++; if(isPartIntact(EnumDriveablePart.leftWing)) numWingsIntact++; float amountOfLift = 2F * g * throttleTemp * numWingsIntact / 2F; if(amountOfLift > g) amountOfLift = g; if(!isPartIntact(EnumDriveablePart.tail)) amountOfLift *= 0.75F; motionY += amountOfLift; //Cut out some motion for correction motionX *= 1F - proportionOfMotionToCorrect; motionY *= 1F - proportionOfMotionToCorrect; motionZ *= 1F - proportionOfMotionToCorrect; //Add the corrected motion motionX += proportionOfMotionToCorrect * newSpeed * forwards.x; motionY += proportionOfMotionToCorrect * newSpeed * forwards.y; motionZ += proportionOfMotionToCorrect * newSpeed * forwards.z; //Apply drag motionX *= drag; motionY *= drag; motionZ *= drag; data.fuelInTank -= throttleScaled * fuelConsumptionMultiplier * data.engine.fuelConsumption; break; default: break; } double motion = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); if(motion > 10) { motionX *= 10 / motion; motionY *= 10 / motion; motionZ *= 10 / motion; } for(EntityWheel wheel : wheels) { if(wheel != null && worldObj != null) { wheel.prevPosX = wheel.posX; wheel.prevPosY = wheel.posY; wheel.prevPosZ = wheel.posZ; } } for(EntityWheel wheel : wheels) { if(wheel != null && worldObj != null) if(type.floatOnWater && worldObj.isAnyLiquid(wheel.getEntityBoundingBox())) { motionY += type.buoyancy; } } //Move the wheels first for(EntityWheel wheel : wheels) { if(wheel != null) { wheel.prevPosY = wheel.posY; wheel.moveEntity(motionX, motionY, motionZ); } } //Update wheels for(int i = 0; i < 2; i++) { Vector3f amountToMoveCar = new Vector3f(motionX / 2F, motionY / 2F, motionZ / 2F); for(EntityWheel wheel : wheels) { if(wheel == null) continue; //Hacky way of forcing the car to step up blocks onGround = true; wheel.onGround = true; //Update angles wheel.rotationYaw = axes.getYaw(); //Pull wheels towards car Vector3f targetWheelPos = axes.findLocalVectorGlobally(getPlaneType().wheelPositions[wheel.ID].position); Vector3f currentWheelPos = new Vector3f(wheel.posX - posX, wheel.posY - posY, wheel.posZ - posZ); float targetWheelLength = targetWheelPos.length(); float currentWheelLength = currentWheelPos.length(); float dLength = targetWheelLength - currentWheelLength; float dAngle = Vector3f.angle(targetWheelPos, currentWheelPos); { //Now Lerp by wheelSpringStrength and work out the new positions float newLength = currentWheelLength + dLength * type.wheelSpringStrength; Vector3f rotateAround = Vector3f.cross(targetWheelPos, currentWheelPos, null); Matrix4f mat = new Matrix4f(); mat.m00 = currentWheelPos.x; mat.m10 = currentWheelPos.y; mat.m20 = currentWheelPos.z; mat.rotate(dAngle * type.wheelSpringStrength, rotateAround); axes.rotateGlobal(-dAngle * type.wheelSpringStrength, rotateAround); Vector3f newWheelPos = new Vector3f(mat.m00, mat.m10, mat.m20); newWheelPos.normalise().scale(newLength); //The proportion of the spring adjustment that is applied to the wheel. 1 - this is applied to the plane float wheelProportion = 0.75F; //wheel.motionX = (newWheelPos.x - currentWheelPos.x) * wheelProportion; //wheel.motionY = (newWheelPos.y - currentWheelPos.y) * wheelProportion; //wheel.motionZ = (newWheelPos.z - currentWheelPos.z) * wheelProportion; Vector3f amountToMoveWheel = new Vector3f(); amountToMoveWheel.x = (newWheelPos.x - currentWheelPos.x) * (1F - wheelProportion); amountToMoveWheel.y = (newWheelPos.y - currentWheelPos.y) * (1F - wheelProportion); amountToMoveWheel.z = (newWheelPos.z - currentWheelPos.z) * (1F - wheelProportion); amountToMoveCar.x -= (newWheelPos.x - currentWheelPos.x) * (1F - wheelProportion); amountToMoveCar.y -= (newWheelPos.y - currentWheelPos.y) * (1F - wheelProportion); amountToMoveCar.z -= (newWheelPos.z - currentWheelPos.z) * (1F - wheelProportion); //The difference between how much the wheel moved and how much it was meant to move. i.e. the reaction force from the block //amountToMoveCar.x += ((wheel.posX - wheel.prevPosX) - (motionX)) * 0.616F / wheels.length; amountToMoveCar.y += ((wheel.posY - wheel.prevPosY) - (motionY)) * 0.5F / wheels.length; //amountToMoveCar.z += ((wheel.posZ - wheel.prevPosZ) - (motionZ)) * 0.0616F / wheels.length; wheel.moveEntity(amountToMoveWheel.x, amountToMoveWheel.y, amountToMoveWheel.z); } } moveEntity(amountToMoveCar.x, amountToMoveCar.y, amountToMoveCar.z); } checkForCollisions(); //Sounds //Starting sound if (throttle > 0.01F && throttle < 0.2F && soundPosition == 0 && hasEnoughFuel()) { PacketPlaySound.sendSoundPacket(posX, posY, posZ, FlansMod.soundRange, dimension, type.startSound, false); soundPosition = type.startSoundLength; } //Flying sound if (throttle > 0.2F && soundPosition == 0 && hasEnoughFuel()) { PacketPlaySound.sendSoundPacket(posX, posY, posZ, FlansMod.soundRange, dimension, type.engineSound, false); soundPosition = type.engineSoundLength; } //Sound decrementer if(soundPosition > 0) soundPosition--; for(EntitySeat seat : seats) { if(seat != null) seat.updatePosition(); } //Calculate movement on the client and then send position, rotation etc to the server if(thePlayerIsDrivingThis) { FlansMod.getPacketHandler().sendToServer(new PacketPlaneControl(this)); serverPosX = posX; serverPosY = posY; serverPosZ = posZ; serverYaw = axes.getYaw(); } //If this is the server, send position updates to everyone, having received them from the driver float updateSpeed = 0.01F; if(!worldObj.isRemote)// && (Math.abs(posX - prevPosX) > updateSpeed || Math.abs(posY - prevPosY) > updateSpeed || Math.abs(posZ - prevPosZ) > updateSpeed)) { FlansMod.getPacketHandler().sendToAllAround(new PacketPlaneControl(this), posX, posY, posZ, FlansMod.driveableUpdateRange, dimension); } } public boolean canThrust() { return (seats[0] != null && seats[0].riddenByEntity instanceof EntityPlayer && ((EntityPlayer)seats[0].riddenByEntity).capabilities.isCreativeMode) || driveableData.fuelInTank > 0; } @Override public void setDead() { super.setDead(); for(EntityWheel wheel : wheels) if(wheel != null) wheel.setDead(); } @Override public boolean gearDown() { return varGear; } private boolean hasWorkingProp() { PlaneType type = getPlaneType(); if(type.mode == EnumPlaneMode.HELI || type.mode == EnumPlaneMode.VTOL) for(Propeller prop : type.heliPropellers) if(isPartIntact(prop.planePart)) return true; if(type.mode == EnumPlaneMode.PLANE || type.mode == EnumPlaneMode.VTOL) for(Propeller prop : type.propellers) if(isPartIntact(prop.planePart)) return true; return false; } public boolean attackEntityFrom(DamageSource damagesource, float i, boolean doDamage) { if(worldObj.isRemote || isDead) return true; PlaneType type = PlaneType.getPlane(driveableType); if(damagesource.damageType.equals("player") && damagesource.getEntity().onGround && (seats[0] == null || seats[0].riddenByEntity == null)) { ItemStack planeStack = new ItemStack(type.item, 1, driveableData.paintjobID); NBTTagCompound tags = new NBTTagCompound(); planeStack.setTagCompound(tags); driveableData.writeToNBT(tags); entityDropItem(planeStack, 0.5F); setDead(); } return true; } @Override public boolean canHitPart(EnumDriveablePart part) { return varGear || (part != EnumDriveablePart.coreWheel && part != EnumDriveablePart.leftWingWheel && part != EnumDriveablePart.rightWingWheel && part != EnumDriveablePart.tailWheel); } @Override public boolean attackEntityFrom(DamageSource damagesource, float i) { return attackEntityFrom(damagesource, i, true); } public PlaneType getPlaneType() { return PlaneType.getPlane(driveableType); } @Override protected void dropItemsOnPartDeath(Vector3f midpoint, DriveablePart part) { } @Override public String getBombInventoryName() { return "Bombs"; } @Override public String getMissileInventoryName() { return "Missiles"; } @Override public boolean hasMouseControlMode() { return true; } }