package org.terasology.componentSystem.controllers; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.vecmath.Quat4f; import javax.vecmath.Vector3d; import javax.vecmath.Vector3f; import org.terasology.componentSystem.RenderSystem; import org.terasology.components.CharacterMovementComponent; import org.terasology.components.HealthComponent; import org.terasology.components.InventoryComponent; import org.terasology.components.ItemComponent; import org.terasology.components.LocalPlayerComponent; import org.terasology.components.PlayerComponent; import org.terasology.components.world.BlockComponent; import org.terasology.components.world.LocationComponent; import org.terasology.entitySystem.EntityRef; import org.terasology.entitySystem.EventHandlerSystem; import org.terasology.entitySystem.ReceiveEvent; import org.terasology.events.ActivateEvent; import org.terasology.events.DamageEvent; import org.terasology.events.NoHealthEvent; import org.terasology.events.input.MouseXAxisEvent; import org.terasology.events.input.MouseYAxisEvent; import org.terasology.events.input.binds.AttackButton; import org.terasology.events.input.binds.ForwardsMovementAxis; import org.terasology.events.input.binds.FrobButton; import org.terasology.events.input.binds.JumpButton; import org.terasology.events.input.binds.RunButton; import org.terasology.events.input.binds.StrafeMovementAxis; import org.terasology.events.input.binds.ToolbarNextButton; import org.terasology.events.input.binds.ToolbarPrevButton; import org.terasology.events.input.binds.ToolbarSlotButton; import org.terasology.events.input.binds.UseItemButton; import org.terasology.events.input.binds.VerticalMovementAxis; import org.terasology.game.CoreRegistry; import org.terasology.input.ButtonState; import org.terasology.logic.LocalPlayer; import org.terasology.logic.manager.Config; import org.terasology.logic.world.BlockEntityRegistry; import org.terasology.logic.world.WorldProvider; import org.terasology.math.TeraMath; import org.terasology.math.Vector3i; import org.terasology.model.structures.RayBlockIntersection; import org.terasology.rendering.cameras.DefaultCamera; import org.terasology.teraspout.TeraBlock; import com.bulletphysics.linearmath.QuaternionUtil; /** * @author Immortius <immortius@gmail.com> */ // TODO: This needs a really good cleanup // TODO: Move more input stuff to a specific input system? // TODO: Camera should become an entity/component, so it can follow the player naturally public class LocalPlayerSystem implements RenderSystem, EventHandlerSystem { private LocalPlayer localPlayer; private WorldProvider worldProvider; private DefaultCamera playerCamera; private long lastTimeSpacePressed; private long lastInteraction; private boolean cameraBobbing = Config.getInstance().isCameraBobbing(); private float bobFactor = 0; private float lastStepDelta = 0; private Vector3f relativeMovement = new Vector3f(); @Override public void initialise() { worldProvider = CoreRegistry.get(WorldProvider.class); localPlayer = CoreRegistry.get(LocalPlayer.class); } @Override public void shutdown() { } public void setPlayerCamera(DefaultCamera camera) { playerCamera = camera; } @ReceiveEvent(components = LocalPlayerComponent.class) public void onMouseX(MouseXAxisEvent event, EntityRef entity) { LocalPlayerComponent localPlayer = entity.getComponent(LocalPlayerComponent.class); localPlayer.viewYaw = (localPlayer.viewYaw - event.getValue()) % 360; entity.saveComponent(localPlayer); LocationComponent loc = entity.getComponent(LocationComponent.class); if (loc != null) { QuaternionUtil.setEuler(loc.getLocalRotation(), TeraMath.DEG_TO_RAD * localPlayer.viewYaw, 0, 0); entity.saveComponent(loc); } event.consume(); } @ReceiveEvent(components = LocalPlayerComponent.class) public void onMouseY(MouseYAxisEvent event, EntityRef entity) { LocalPlayerComponent localPlayer = entity.getComponent(LocalPlayerComponent.class); localPlayer.viewPitch = TeraMath.clamp(localPlayer.viewPitch - event.getValue(), -89, 89); entity.saveComponent(localPlayer); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class, CharacterMovementComponent.class}) public void onJump(JumpButton event, EntityRef entity) { if (event.getState() == ButtonState.DOWN) { CharacterMovementComponent characterMovement = entity.getComponent(CharacterMovementComponent.class); characterMovement.jump = true; if (System.currentTimeMillis() - lastTimeSpacePressed < 200) { characterMovement.isGhosting = !characterMovement.isGhosting; } lastTimeSpacePressed = System.currentTimeMillis(); event.consume(); } } @ReceiveEvent(components = {LocalPlayerComponent.class, CharacterMovementComponent.class}) public void updateForwardsMovement(ForwardsMovementAxis event, EntityRef entity) { relativeMovement.z = event.getValue(); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class, CharacterMovementComponent.class}) public void updateStrafeMovement(StrafeMovementAxis event, EntityRef entity) { relativeMovement.x = event.getValue(); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class, CharacterMovementComponent.class}) public void updateVerticalMovement(VerticalMovementAxis event, EntityRef entity) { relativeMovement.y = event.getValue(); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class, CharacterMovementComponent.class}) public void onRun(RunButton event, EntityRef entity) { CharacterMovementComponent characterMovement = entity.getComponent(CharacterMovementComponent.class); characterMovement.isRunning = event.isDown(); event.consume(); } @Override public void renderOverlay() { // TODO: Don't render if not in first person? // Display the block the player is aiming at if (Config.getInstance().isPlacingBox()) { RayBlockIntersection.Intersection selectedBlock = calcSelectedBlock(); if (selectedBlock != null) { TeraBlock block = worldProvider.getBlock(selectedBlock.getBlockPosition()); if (block.isRenderBoundingBox()) { block.getBounds(selectedBlock.getBlockPosition()).render(2f); } } } } @ReceiveEvent(components = {LocalPlayerComponent.class}) public void onDeath(NoHealthEvent event, EntityRef entity) { LocalPlayerComponent localPlayer = entity.getComponent(LocalPlayerComponent.class); localPlayer.isDead = true; localPlayer.respawnWait = 1.0f; entity.saveComponent(localPlayer); } private void updateMovement(LocalPlayerComponent localPlayerComponent, CharacterMovementComponent characterMovementComponent, LocationComponent location) { Vector3f relMove = new Vector3f(relativeMovement); relMove.y = 0; if (characterMovementComponent.isGhosting || characterMovementComponent.isSwimming) { Quat4f viewRot = new Quat4f(); QuaternionUtil.setEuler(viewRot, TeraMath.DEG_TO_RAD * localPlayerComponent.viewYaw, TeraMath.DEG_TO_RAD * localPlayerComponent.viewPitch, 0); QuaternionUtil.quatRotate(viewRot, relMove, relMove); relMove.y += relativeMovement.y; } else { QuaternionUtil.quatRotate(location.getLocalRotation(), relMove, relMove); } float lengthSquared = relMove.lengthSquared(); if (lengthSquared > 1) relMove.normalize(); characterMovementComponent.setDrive(relMove); } private boolean checkRespawn(float deltaSeconds, EntityRef entity, LocalPlayerComponent localPlayerComponent, CharacterMovementComponent characterMovementComponent, LocationComponent location, PlayerComponent playerComponent) { localPlayerComponent.respawnWait -= deltaSeconds; if (localPlayerComponent.respawnWait > 0) { characterMovementComponent.getDrive().set(0, 0, 0); characterMovementComponent.jump = false; return false; } // Respawn localPlayerComponent.isDead = false; HealthComponent health = entity.getComponent(HealthComponent.class); if (health != null) { health.currentHealth = health.maxHealth; entity.saveComponent(health); } location.setWorldPosition(playerComponent.spawnPosition); entity.saveComponent(location); return true; } private void updateCamera(CharacterMovementComponent charMovementComp, Vector3f position, Quat4f rotation) { // The camera position is the player's position plus the eye offset Vector3d cameraPosition = new Vector3d(); // TODO: don't hardset eye position cameraPosition.add(new Vector3d(position), new Vector3d(0, 0.6f, 0)); playerCamera.getPosition().set(cameraPosition); Vector3f viewDir = new Vector3f(0, 0, 1); QuaternionUtil.quatRotate(rotation, viewDir, viewDir); playerCamera.getViewingDirection().set(viewDir); float stepDelta = charMovementComp.footstepDelta - lastStepDelta; if (stepDelta < 0) stepDelta += charMovementComp.distanceBetweenFootsteps; bobFactor += stepDelta; lastStepDelta = charMovementComp.footstepDelta; if (cameraBobbing) { playerCamera.setBobbingRotationOffsetFactor(calcBobbingOffset(0.0f, 0.01f, 2.5f)); playerCamera.setBobbingVerticalOffsetFactor(calcBobbingOffset((float) java.lang.Math.PI / 4f, 0.025f, 3f)); } else { playerCamera.setBobbingRotationOffsetFactor(0.0); playerCamera.setBobbingVerticalOffsetFactor(0.0); } if (charMovementComp.isGhosting) { playerCamera.extendFov(24); } else { playerCamera.resetFov(); } } @ReceiveEvent(components = {LocalPlayerComponent.class, InventoryComponent.class}) public void onAttackRequest(AttackButton event, EntityRef entity) { if (!event.isDown() || System.currentTimeMillis() - lastInteraction < 200) { return; } LocalPlayerComponent localPlayerComp = entity.getComponent(LocalPlayerComponent.class); InventoryComponent inventory = entity.getComponent(InventoryComponent.class); if (localPlayerComp.isDead) return; EntityRef selectedItemEntity = inventory.itemSlots.get(localPlayerComp.selectedTool); attack(event.getTarget(), entity, selectedItemEntity); lastInteraction = System.currentTimeMillis(); localPlayerComp.handAnimation = 0.5f; entity.saveComponent(localPlayerComp); event.consume(); } private void attack(EntityRef target, EntityRef player, EntityRef selectedItemEntity) { // TODO: Should send an attack event to self, and another system common to all creatures should handle this int damage = 1; ItemComponent item = selectedItemEntity.getComponent(ItemComponent.class); if (item != null) { damage = item.baseDamage; BlockComponent blockComp = target.getComponent(BlockComponent.class); if (blockComp != null) { TeraBlock block = worldProvider.getBlock(blockComp.getPosition()); if (item.getPerBlockDamageBonus().containsKey(block.getBlockFamily().getTitle())) { damage += item.getPerBlockDamageBonus().get(block.getBlockFamily().getTitle()); } } } target.send(new DamageEvent(damage, player)); } @ReceiveEvent(components = {LocalPlayerComponent.class}) public void onFrobRequest(FrobButton event, EntityRef entity) { if (event.getState() != ButtonState.DOWN) { return; } LocalPlayerComponent localPlayerComp = entity.getComponent(LocalPlayerComponent.class); if (localPlayerComp.isDead) return; event.getTarget().send(new ActivateEvent(entity, entity)); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class}) public void onNextItem(ToolbarNextButton event, EntityRef entity) { LocalPlayerComponent localPlayerComp = localPlayer.getEntity().getComponent(LocalPlayerComponent.class); localPlayerComp.selectedTool = (localPlayerComp.selectedTool + 1) % 9; localPlayer.getEntity().saveComponent(localPlayerComp); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class}) public void onPrevItem(ToolbarPrevButton event, EntityRef entity) { LocalPlayerComponent localPlayerComp = localPlayer.getEntity().getComponent(LocalPlayerComponent.class); localPlayerComp.selectedTool = (localPlayerComp.selectedTool - 1) % 9; if (localPlayerComp.selectedTool < 0) { localPlayerComp.selectedTool = 9 + localPlayerComp.selectedTool; } localPlayer.getEntity().saveComponent(localPlayerComp); event.consume(); } @ReceiveEvent(components = {LocalPlayerComponent.class}) public void onSlotButton(ToolbarSlotButton event, EntityRef entity) { LocalPlayerComponent localPlayerComp = entity.getComponent(LocalPlayerComponent.class); localPlayerComp.selectedTool = event.getSlot(); localPlayer.getEntity().saveComponent(localPlayerComp); } @ReceiveEvent(components = {LocalPlayerComponent.class, InventoryComponent.class}) public void onUseItemRequest(UseItemButton event, EntityRef entity) { if (!event.isDown() || System.currentTimeMillis() - lastInteraction < 200) { return; } LocalPlayerComponent localPlayerComp = entity.getComponent(LocalPlayerComponent.class); InventoryComponent inventory = entity.getComponent(InventoryComponent.class); if (localPlayerComp.isDead) return; EntityRef selectedItemEntity = inventory.itemSlots.get(localPlayerComp.selectedTool); ItemComponent item = selectedItemEntity.getComponent(ItemComponent.class); if (item != null && item.usage != ItemComponent.UsageType.NONE) { useItem(entity, selectedItemEntity); } else { attack(event.getTarget(), entity, selectedItemEntity); } lastInteraction = System.currentTimeMillis(); localPlayerComp.handAnimation = 0.5f; entity.saveComponent(localPlayerComp); event.consume(); } private void useItem(EntityRef player, EntityRef item) { // TODO: Raytrace against entities too // TODO: Or should more information be included with events (surface normal?) // TODO: Do we even need the surface normal? RayBlockIntersection.Intersection blockIntersection = calcSelectedBlock(); if (blockIntersection != null) { Vector3i centerPos = blockIntersection.getBlockPosition(); item.send(new ActivateEvent(CoreRegistry.get(BlockEntityRegistry.class).getOrCreateEntityAt(centerPos), player, new Vector3f(playerCamera.getPosition()), new Vector3f(playerCamera.getViewingDirection()), blockIntersection.getSurfaceNormal())); } else { item.send(new ActivateEvent(player, new Vector3f(playerCamera.getPosition()), new Vector3f(playerCamera.getViewingDirection()))); } } /** * Calculates the currently targeted block in front of the player. * * @return Intersection point of the targeted block */ private RayBlockIntersection.Intersection calcSelectedBlock() { // TODO: Proper and centralised ray tracing support though world List<RayBlockIntersection.Intersection> inters = new ArrayList<RayBlockIntersection.Intersection>(); Vector3f pos = new Vector3f(playerCamera.getPosition()); int blockPosX, blockPosY, blockPosZ; for (int x = -3; x <= 3; x++) { for (int y = -3; y <= 3; y++) { for (int z = -3; z <= 3; z++) { // Make sure the correct block positions are calculated relatively to the position of the player blockPosX = (int) (pos.x + (pos.x >= 0 ? 0.5f : -0.5f)) + x; blockPosY = (int) (pos.y + (pos.y >= 0 ? 0.5f : -0.5f)) + y; blockPosZ = (int) (pos.z + (pos.z >= 0 ? 0.5f : -0.5f)) + z; TeraBlock block = worldProvider.getBlock(blockPosX, blockPosY, blockPosZ); // Ignore special blocks if (block.isSelectionRayThrough()) { continue; } // The ray originates from the "player's eye" List<RayBlockIntersection.Intersection> iss = RayBlockIntersection.executeIntersection(worldProvider, blockPosX, blockPosY, blockPosZ, playerCamera.getPosition(), playerCamera.getViewingDirection()); if (iss != null) { inters.addAll(iss); } } } } /** * Calculated the closest intersection. */ if (inters.size() > 0) { Collections.sort(inters); return inters.get(0); } return null; } private double calcBobbingOffset(float phaseOffset, float amplitude, float frequency) { return java.lang.Math.sin(bobFactor * frequency + phaseOffset) * amplitude; } @Override public void renderOpaque() { } @Override public void renderTransparent() { } @Override public void renderFirstPerson() { } }