diff --git a/app/build.gradle b/app/build.gradle index 75d9dc6..2866ff8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,5 +40,5 @@ java { application { // Define the main class for the application. - mainClass = 'com.vincentramdhanie.twod.game.Main' + mainClass = 'com.niravramdhanie.twod.game.Main' } diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/Main.java b/app/src/main/java/com/niravramdhanie/twod/game/Main.java similarity index 84% rename from app/src/main/java/com/vincentramdhanie/twod/game/Main.java rename to app/src/main/java/com/niravramdhanie/twod/game/Main.java index 9019ee5..ddaf64a 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/Main.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/Main.java @@ -1,8 +1,9 @@ -package com.vincentramdhanie.twod.game; +package com.niravramdhanie.twod.game; -import com.vincentramdhanie.twod.game.core.Game; import javax.swing.SwingUtilities; +import com.niravramdhanie.twod.game.core.Game; + public class Main { public static void main(String[] args) { System.out.println("Starting the game application..."); @@ -15,7 +16,7 @@ public static void main(String[] args) { try { // Create the game System.out.println("Creating game instance"); - Game game = new Game("My 2D Game", 800, 600); + Game game = new Game("My 2D Game", 1000, 750); // Game will start itself when initialization is complete System.out.println("Game instance created"); diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/Action.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/Action.java new file mode 100644 index 0000000..da853e2 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/Action.java @@ -0,0 +1,18 @@ +package com.niravramdhanie.twod.game.actions; + +/** + * Interface for actions that can be executed when a button is pressed. + */ +public interface Action { + /** + * Executes the action. + */ + void execute(); + + /** + * Gets a description of the action. + * + * @return A description of what the action does + */ + String getDescription(); +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/ActionFactory.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/ActionFactory.java new file mode 100644 index 0000000..363671d --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/ActionFactory.java @@ -0,0 +1,159 @@ +package com.niravramdhanie.twod.game.actions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import com.niravramdhanie.twod.game.entity.Button; + +/** + * Factory class for creating different types of actions for buttons. + */ +public class ActionFactory { + private static final Random random = new Random(); + + /** + * Action types enum + */ + public enum ActionType { + MESSAGE, + TIMED, + TOGGLE, + DOOR, + MULTI, + CYCLING_MESSAGE, + TIMED_TOGGLE, // A timed action that toggles + COMBINED // Multiple actions combined + } + + /** + * Creates an action of the specified type. + * + * @param type The type of action to create + * @param button The button that will use this action (needed for timed actions) + * @return The created action + */ + public static Action createAction(ActionType type, Button button) { + switch (type) { + case MESSAGE: + return new MessageAction("Button pressed: " + type.name()); + + case CYCLING_MESSAGE: + return new MessageAction(true, + "This is message 1", + "This is message 2", + "This is message 3", + "Press again to cycle to the next message"); + + case TIMED: + MessageAction msgAction = new MessageAction("Timed button pressed! Will deactivate in 3 seconds."); + TimedAction timedAction = new TimedAction(msgAction, 3000); + timedAction.setTargetButton(button); + return timedAction; + + case TOGGLE: + DoorAction doorAction = new DoorAction("toggle_door_" + random.nextInt(1000)); + doorAction.setDoorStateChangeListener((doorId, isOpen) -> { + System.out.println("Door listener: Door " + doorId + " is now " + (isOpen ? "open" : "closed")); + }); + return new ToggleAction(doorAction, doorAction); + + case DOOR: + DoorAction simpleDoor = new DoorAction("simple_door_" + random.nextInt(1000)); + return simpleDoor; + + case MULTI: + // Create a multi-action with several different actions + MessageAction firstAction = new MessageAction("First action executed!"); + MessageAction secondAction = new MessageAction("Second action executed!"); + DoorAction thirdAction = new DoorAction("multi_door_" + random.nextInt(1000)); + thirdAction.setDoorStateChangeListener((doorId, isOpen) -> { + System.out.println("MultiAction door: " + doorId + " is now " + (isOpen ? "open" : "closed")); + }); + + return new MultiAction(firstAction, secondAction, thirdAction); + + case TIMED_TOGGLE: + // Create a timed action that wraps a toggle action + ToggleAction toggleAction = new ToggleAction( + new MessageAction("Toggled OFF"), + new MessageAction("Toggled ON") + ); + TimedAction timedToggle = new TimedAction(toggleAction, 5000); + timedToggle.setTargetButton(button); + return timedToggle; + + case COMBINED: + // Create a combination of different action types + // This example creates a multi-action with cycling messages and a door action + MessageAction cyclingMsg = new MessageAction(true, + "Combined action - Message 1", + "Combined action - Message 2", + "Combined action - Message 3"); + + DoorAction combinedDoor = new DoorAction("combined_door_" + random.nextInt(1000)); + combinedDoor.setDoorStateChangeListener((doorId, isOpen) -> { + System.out.println("Combined door: " + doorId + " is now " + (isOpen ? "open" : "closed")); + }); + + return new MultiAction(cyclingMsg, combinedDoor); + + default: + return new MessageAction("Default action"); + } + } + + /** + * Creates a random action. + * + * @param button The button that will use this action + * @return A randomly selected action + */ + public static Action createRandomAction(Button button) { + ActionType[] types = ActionType.values(); + ActionType randomType = types[random.nextInt(types.length)]; + + return createAction(randomType, button); + } + + /** + * Creates one of each action type. + * + * @param button The button that will use these actions (for timed actions) + * @return A list containing one of each action type + */ + public static List createAllActionTypes(Button button) { + List actions = new ArrayList<>(); + + for (ActionType type : ActionType.values()) { + actions.add(createAction(type, button)); + } + + return actions; + } + + /** + * Creates a timed message action. + * + * @param message The message to display + * @param durationMillis The duration in milliseconds + * @param button The button to deactivate + * @return A timed message action + */ + public static TimedAction createTimedMessage(String message, int durationMillis, Button button) { + MessageAction msgAction = new MessageAction(message); + TimedAction timedAction = new TimedAction(msgAction, durationMillis); + timedAction.setTargetButton(button); + return timedAction; + } + + /** + * Creates a cycling message action. + * + * @param messages The messages to cycle through + * @return A cycling message action + */ + public static MessageAction createCyclingMessage(String... messages) { + return new MessageAction(true, messages); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorAction.java new file mode 100644 index 0000000..e51b032 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorAction.java @@ -0,0 +1,104 @@ +package com.niravramdhanie.twod.game.actions; + +/** + * An action that simulates opening or closing a door. + */ +public class DoorAction implements Action { + private String doorId; + private boolean doorOpen; + private DoorStateChangeListener listener; + + /** + * Creates a new door action. + * + * @param doorId The ID of the door + */ + public DoorAction(String doorId) { + this.doorId = doorId; + this.doorOpen = false; + this.listener = null; + } + + @Override + public void execute() { + // Toggle the door state + doorOpen = !doorOpen; + + // Notify the listener if present + if (listener != null) { + listener.onDoorStateChanged(doorId, doorOpen); + } + + // Print a message to the console + System.out.println("Door " + doorId + " is now " + (doorOpen ? "open" : "closed")); + } + + @Override + public String getDescription() { + return "Door '" + doorId + "' - " + (doorOpen ? "OPEN" : "CLOSED"); + } + + /** + * Interface for components that need to be notified of door state changes. + */ + public interface DoorStateChangeListener { + /** + * Called when a door's state changes. + * + * @param doorId The ID of the door + * @param isOpen True if the door is now open, false if closed + */ + void onDoorStateChanged(String doorId, boolean isOpen); + } + + /** + * Sets the listener for door state changes. + * + * @param listener The listener to notify when the door state changes + */ + public void setDoorStateChangeListener(DoorStateChangeListener listener) { + this.listener = listener; + } + + /** + * Gets the door ID. + * + * @return The door ID + */ + public String getDoorId() { + return doorId; + } + + /** + * Sets the door ID. + * + * @param doorId The door ID + */ + public void setDoorId(String doorId) { + this.doorId = doorId; + } + + /** + * Checks if the door is open. + * + * @return True if the door is open, false if closed + */ + public boolean isDoorOpen() { + return doorOpen; + } + + /** + * Sets the door state. + * + * @param doorOpen True to set as open, false for closed + */ + public void setDoorOpen(boolean doorOpen) { + boolean oldState = this.doorOpen; + this.doorOpen = doorOpen; + + // If the state changed and we have a listener, notify it + if (oldState != doorOpen && listener != null) { + listener.onDoorStateChanged(doorId, doorOpen); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorController.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorController.java new file mode 100644 index 0000000..1003458 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/DoorController.java @@ -0,0 +1,167 @@ +package com.niravramdhanie.twod.game.actions; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.niravramdhanie.twod.game.entity.Door; + +/** + * Controls and manages doors in the game. + * Tracks door states and provides methods to open/close doors by ID. + */ +public class DoorController implements DoorAction.DoorStateChangeListener { + private List doors; + private Map doorMap; + + /** + * Creates a new door controller. + */ + public DoorController() { + doors = new ArrayList<>(); + doorMap = new HashMap<>(); + } + + /** + * Registers a door with the controller. + * + * @param door The door to register + */ + public void registerDoor(Door door) { + doors.add(door); + doorMap.put(door.getId(), door); + + // Add this controller as a listener to the door + for (DoorAction action : findDoorActions(door.getId())) { + action.setDoorStateChangeListener(this); + } + } + + /** + * Finds all door actions for a specific door ID. + * + * @param doorId The door ID + * @return List of door actions + */ + private List findDoorActions(String doorId) { + // This would typically be connected to a button registry or similar, + // but for now, we'll return an empty list and register actions manually + return new ArrayList<>(); + } + + /** + * Opens a door by ID. + * + * @param doorId The ID of the door to open + * @return True if the door was found and opened, false otherwise + */ + public boolean openDoor(String doorId) { + Door door = doorMap.get(doorId); + if (door != null) { + door.open(); + return true; + } + return false; + } + + /** + * Closes a door by ID. + * + * @param doorId The ID of the door to close + * @return True if the door was found and closed, false otherwise + */ + public boolean closeDoor(String doorId) { + Door door = doorMap.get(doorId); + if (door != null) { + door.close(); + return true; + } + return false; + } + + /** + * Toggles a door's state by ID. + * + * @param doorId The ID of the door to toggle + * @return True if the door was found and toggled, false otherwise + */ + public boolean toggleDoor(String doorId) { + Door door = doorMap.get(doorId); + if (door != null) { + door.toggle(); + return true; + } + return false; + } + + /** + * Closes all doors managed by this controller. + */ + public void closeAllDoors() { + for (Door door : doors) { + door.close(); + } + } + + /** + * Opens all doors managed by this controller. + */ + public void openAllDoors() { + for (Door door : doors) { + door.open(); + } + } + + /** + * Gets all doors managed by this controller. + * + * @return The list of doors + */ + public List getDoors() { + return doors; + } + + /** + * Gets a door by ID. + * + * @param doorId The door ID + * @return The door, or null if not found + */ + public Door getDoor(String doorId) { + return doorMap.get(doorId); + } + + /** + * Removes a door from the controller. + * + * @param doorId The ID of the door to remove + * @return True if the door was found and removed, false otherwise + */ + public boolean removeDoor(String doorId) { + Door door = doorMap.remove(doorId); + if (door != null) { + doors.remove(door); + return true; + } + return false; + } + + @Override + public void onDoorStateChanged(String doorId, boolean isOpen) { + // Propagate the door state change to the actual door + Door door = doorMap.get(doorId); + if (door != null) { + if (isOpen) { + door.open(); + } else { + door.close(); + } + System.out.println("DoorController: Door " + doorId + " is now " + + (isOpen ? "open" : "closed")); + } else { + System.out.println("DoorController: Warning - Door " + doorId + + " not found when trying to change state"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/MessageAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/MessageAction.java new file mode 100644 index 0000000..33d21a7 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/MessageAction.java @@ -0,0 +1,128 @@ +package com.niravramdhanie.twod.game.actions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An enhanced version of PrintAction that can cycle through multiple messages. + */ +public class MessageAction implements Action { + private List messages; + private int currentMessageIndex; + private boolean cycling; + + /** + * Creates a new message action with a single message. + * + * @param message The message to display + */ + public MessageAction(String message) { + this.messages = new ArrayList<>(); + this.messages.add(message); + this.currentMessageIndex = 0; + this.cycling = false; + } + + /** + * Creates a new message action with multiple messages that will cycle when executed. + * + * @param cycling Whether to cycle through messages + * @param messages The messages to cycle through + */ + public MessageAction(boolean cycling, String... messages) { + this.messages = new ArrayList<>(Arrays.asList(messages)); + this.currentMessageIndex = 0; + this.cycling = cycling; + } + + @Override + public void execute() { + // Print the current message + if (!messages.isEmpty()) { + System.out.println(getCurrentMessage()); + + // Move to the next message if cycling is enabled + if (cycling) { + currentMessageIndex = (currentMessageIndex + 1) % messages.size(); + } + } + } + + @Override + public String getDescription() { + if (cycling) { + return "Cycling message (" + (currentMessageIndex + 1) + "/" + messages.size() + ")"; + } else { + return "Message: " + getCurrentMessage(); + } + } + + /** + * Gets the current message. + * + * @return The current message + */ + public String getCurrentMessage() { + if (messages.isEmpty()) { + return "No message"; + } + return messages.get(currentMessageIndex); + } + + /** + * Gets all messages. + * + * @return The list of messages + */ + public List getMessages() { + return messages; + } + + /** + * Adds a message to the list. + * + * @param message The message to add + */ + public void addMessage(String message) { + messages.add(message); + } + + /** + * Sets whether the action should cycle through messages. + * + * @param cycling True to cycle, false to stay on the current message + */ + public void setCycling(boolean cycling) { + this.cycling = cycling; + } + + /** + * Checks if the action is cycling through messages. + * + * @return True if cycling, false otherwise + */ + public boolean isCycling() { + return cycling; + } + + /** + * Gets the current message index. + * + * @return The current message index + */ + public int getCurrentMessageIndex() { + return currentMessageIndex; + } + + /** + * Sets the current message index. + * + * @param index The index to set + */ + public void setCurrentMessageIndex(int index) { + if (index >= 0 && index < messages.size()) { + currentMessageIndex = index; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiAction.java new file mode 100644 index 0000000..b4713e2 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiAction.java @@ -0,0 +1,104 @@ +package com.niravramdhanie.twod.game.actions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An action that executes multiple actions in sequence. + */ +public class MultiAction implements Action { + private List actions; + + /** + * Creates a new multi-action with the given actions. + * + * @param actions The actions to execute in sequence + */ + public MultiAction(Action... actions) { + this.actions = new ArrayList<>(Arrays.asList(actions)); + } + + /** + * Creates a new multi-action with a list of actions. + * + * @param actions The list of actions to execute + */ + public MultiAction(List actions) { + this.actions = new ArrayList<>(actions); + } + + @Override + public void execute() { + // Execute each action in sequence + for (Action action : actions) { + if (action != null) { + action.execute(); + } + } + } + + @Override + public String getDescription() { + StringBuilder description = new StringBuilder("Multi-action ("); + description.append(actions.size()).append(" actions)"); + + // Add descriptions of individual actions if there aren't too many + if (actions.size() <= 3) { + description.append(": "); + for (int i = 0; i < actions.size(); i++) { + if (i > 0) { + description.append(" → "); + } + Action action = actions.get(i); + description.append(action != null ? action.getDescription() : "null"); + } + } + + return description.toString(); + } + + /** + * Adds an action to the sequence. + * + * @param action The action to add + */ + public void addAction(Action action) { + actions.add(action); + } + + /** + * Removes an action from the sequence. + * + * @param action The action to remove + * @return True if the action was found and removed, false otherwise + */ + public boolean removeAction(Action action) { + return actions.remove(action); + } + + /** + * Gets all actions in the sequence. + * + * @return The list of actions + */ + public List getActions() { + return actions; + } + + /** + * Clears all actions. + */ + public void clearActions() { + actions.clear(); + } + + /** + * Gets the number of actions. + * + * @return The number of actions + */ + public int getActionCount() { + return actions.size(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiButtonAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiButtonAction.java new file mode 100644 index 0000000..c6e9b51 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/MultiButtonAction.java @@ -0,0 +1,174 @@ +package com.niravramdhanie.twod.game.actions; + +import java.util.ArrayList; +import java.util.List; + +/** + * An action that only executes when all required buttons are activated. + * Used for scenarios where multiple buttons need to be pressed simultaneously. + */ +public class MultiButtonAction implements Action { + private List requiredButtonIds; + private List buttonStates; + private Action targetAction; + private boolean permanentActivation; + private boolean wasActivated; + + /** + * Creates a new MultiButtonAction. + * + * @param targetAction The action to execute when all buttons are activated + * @param permanentActivation If true, once activated, the action stays activated even if buttons are released + */ + public MultiButtonAction(Action targetAction, boolean permanentActivation) { + this.targetAction = targetAction; + this.requiredButtonIds = new ArrayList<>(); + this.buttonStates = new ArrayList<>(); + this.permanentActivation = permanentActivation; + this.wasActivated = false; + } + + /** + * Adds a button requirement. + * + * @param buttonId The ID of the required button + */ + public void addRequiredButton(String buttonId) { + requiredButtonIds.add(buttonId); + buttonStates.add(false); + } + + /** + * Updates the state of a button. + * + * @param buttonId The ID of the button + * @param isActivated Whether the button is activated + * @return True if the update changed the overall activation state, false otherwise + */ + public boolean updateButtonState(String buttonId, boolean isActivated) { + int index = requiredButtonIds.indexOf(buttonId); + if (index == -1) { + System.out.println("Button ID not found: " + buttonId); + return false; + } + + // Update the button state + boolean oldState = buttonStates.get(index); + buttonStates.set(index, isActivated); + + boolean stateChanged = oldState != isActivated; + if (stateChanged) { + System.out.println("Button " + buttonId + " is now " + (isActivated ? "activated" : "deactivated")); + } + + // Check if all buttons are now activated + boolean allActivated = checkAllButtonsActivated(); + + if (allActivated) { + System.out.println("All buttons are now activated!"); + + // If this is the first time all buttons are activated, record it + if (!wasActivated) { + wasActivated = true; + System.out.println("Permanent activation achieved!"); + } + } else { + // Log which buttons are still needed + if (isActivated) { + logRemainingButtons(); + } + } + + // Execute the target action if we're activated + if ((allActivated || (permanentActivation && wasActivated)) && targetAction != null) { + if (allActivated) { + System.out.println("Executing target action because all buttons are active"); + } else { + System.out.println("Executing target action because of permanent activation"); + } + targetAction.execute(); + return true; + } + + return false; + } + + /** + * Logs information about which buttons still need to be activated. + */ + private void logRemainingButtons() { + List missingButtons = new ArrayList<>(); + for (int i = 0; i < requiredButtonIds.size(); i++) { + if (!buttonStates.get(i)) { + missingButtons.add(requiredButtonIds.get(i)); + } + } + + if (!missingButtons.isEmpty()) { + System.out.println("Still need to activate: " + String.join(", ", missingButtons)); + } + } + + /** + * Checks if all required buttons are activated. + * + * @return True if all buttons are activated, false otherwise + */ + private boolean checkAllButtonsActivated() { + for (Boolean state : buttonStates) { + if (!state) { + return false; + } + } + return !buttonStates.isEmpty(); + } + + @Override + public void execute() { + // This action doesn't directly execute; it's controlled by button states + // The actual execution happens in updateButtonState + } + + @Override + public String getDescription() { + int totalButtons = requiredButtonIds.size(); + int activatedButtons = 0; + + for (Boolean state : buttonStates) { + if (state) { + activatedButtons++; + } + } + + String actionDesc = targetAction != null ? targetAction.getDescription() : "No action"; + return "Multi-button (" + activatedButtons + "/" + totalButtons + " active): " + actionDesc; + } + + /** + * Resets the activation state. + */ + public void reset() { + wasActivated = false; + for (int i = 0; i < buttonStates.size(); i++) { + buttonStates.set(i, false); + } + } + + /** + * Gets whether this action has been permanently activated. + * + * @return True if permanently activated, false otherwise + */ + public boolean isPermanentlyActivated() { + return permanentActivation && wasActivated; + } + + /** + * Sets whether this action uses permanent activation. + * + * @param permanentActivation True for permanent activation, false for temporary + */ + public void setPermanentActivation(boolean permanentActivation) { + this.permanentActivation = permanentActivation; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/PrintAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/PrintAction.java new file mode 100644 index 0000000..217d120 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/PrintAction.java @@ -0,0 +1,52 @@ +package com.niravramdhanie.twod.game.actions; + +/** + * A simple action that prints a message to the console. + */ +public class PrintAction implements Action { + private String message; + + /** + * Creates a new print action with the given message. + * + * @param message The message to print + */ + public PrintAction(String message) { + this.message = message; + } + + /** + * Creates a new print action with the default message. + */ + public PrintAction() { + this("Button pressed"); + } + + @Override + public void execute() { + System.out.println(message); + } + + @Override + public String getDescription() { + return "Prints: " + message; + } + + /** + * Sets the message to print. + * + * @param message The new message + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * Gets the message that will be printed. + * + * @return The message + */ + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/TimedAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/TimedAction.java new file mode 100644 index 0000000..238c868 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/TimedAction.java @@ -0,0 +1,168 @@ +package com.niravramdhanie.twod.game.actions; + +import com.niravramdhanie.twod.game.entity.Button; + +/** + * An action that executes for a limited time and then automatically deactivates. + */ +public class TimedAction implements Action { + private Action wrappedAction; + private Action deactivateAction; // Action to execute when deactivated + private long durationMillis; + private boolean isActive; + private long startTime; + private Button targetButton; + + /** + * Creates a new timed action. + * + * @param wrappedAction The action to execute for a limited time + * @param durationMillis The duration in milliseconds + */ + public TimedAction(Action wrappedAction, long durationMillis) { + this.wrappedAction = wrappedAction; + this.durationMillis = durationMillis; + this.isActive = false; + this.startTime = 0; + this.deactivateAction = null; + } + + /** + * Sets the button that should be deactivated when the timer expires. + * + * @param button The button to deactivate + */ + public void setTargetButton(Button button) { + this.targetButton = button; + } + + /** + * Sets an action to execute when this timed action deactivates. + * + * @param deactivateAction The action to execute on deactivation + */ + public void setDeactivateAction(Action deactivateAction) { + this.deactivateAction = deactivateAction; + } + + @Override + public void execute() { + if (!isActive) { + // Start the timer + isActive = true; + startTime = System.currentTimeMillis(); + + // Execute the wrapped action + if (wrappedAction != null) { + wrappedAction.execute(); + } + + System.out.println("Timed action started for " + durationMillis + "ms"); + } else { + // Already active, check if it should be deactivated + long elapsedTime = System.currentTimeMillis() - startTime; + if (elapsedTime >= durationMillis) { + deactivate(); + System.out.println("Timed action expired"); + } else { + // Refresh the timer + startTime = System.currentTimeMillis(); + System.out.println("Timed action refreshed for " + durationMillis + "ms"); + } + } + } + + /** + * Updates the timed action status. Should be called regularly. + * + * @return True if the action is still active, false if it has expired + */ + public boolean update() { + if (isActive) { + long elapsedTime = System.currentTimeMillis() - startTime; + if (elapsedTime >= durationMillis) { + deactivate(); + return false; + } + return true; + } + return false; + } + + /** + * Gets the remaining time in milliseconds. + * + * @return The remaining time, or 0 if not active + */ + public long getRemainingTime() { + if (!isActive) { + return 0; + } + + long elapsedTime = System.currentTimeMillis() - startTime; + long remaining = durationMillis - elapsedTime; + return (remaining > 0) ? remaining : 0; + } + + /** + * Gets the fraction of time remaining (0.0 to 1.0). + * + * @return The fraction of time remaining, or 0 if not active + */ + public float getTimeRemainingFraction() { + if (!isActive || durationMillis <= 0) { + return 0.0f; + } + + return (float)getRemainingTime() / durationMillis; + } + + /** + * Forcefully deactivates the timed action. + */ + public void deactivate() { + isActive = false; + + // Execute the deactivate action if set + if (deactivateAction != null) { + deactivateAction.execute(); + } + + // Deactivate the target button if set + if (targetButton != null) { + targetButton.deactivate(); + } + } + + /** + * Checks if the action is currently active. + * + * @return True if active, false otherwise + */ + public boolean isActive() { + return isActive; + } + + /** + * Sets the active state without executing any actions. + * Useful for rewinding operations. + * + * @param active Whether the action is active + */ + public void setActive(boolean active) { + this.isActive = active; + + // Set the start time to the current time when activating + if (active) { + this.startTime = System.currentTimeMillis(); + } else { + this.startTime = 0; + } + } + + @Override + public String getDescription() { + String baseDesc = wrappedAction != null ? wrappedAction.getDescription() : "No action"; + return "Timed (" + (durationMillis / 1000f) + "s): " + baseDesc; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/actions/ToggleAction.java b/app/src/main/java/com/niravramdhanie/twod/game/actions/ToggleAction.java new file mode 100644 index 0000000..4d56910 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/actions/ToggleAction.java @@ -0,0 +1,113 @@ +package com.niravramdhanie.twod.game.actions; + +/** + * An action that toggles between two different actions. + */ +public class ToggleAction implements Action { + private Action offAction; + private Action onAction; + private boolean toggled; + + /** + * Creates a new toggle action with actions for on and off states. + * + * @param offAction The action to execute when toggled off + * @param onAction The action to execute when toggled on + */ + public ToggleAction(Action offAction, Action onAction) { + this.offAction = offAction; + this.onAction = onAction; + this.toggled = false; + } + + /** + * Creates a toggle action with the same action for both states. + * + * @param action The action to execute on every toggle + */ + public ToggleAction(Action action) { + this(action, action); + } + + @Override + public void execute() { + // Toggle the state + toggled = !toggled; + + // Execute the appropriate action based on the toggled state + if (toggled) { + if (onAction != null) { + onAction.execute(); + } + } else { + if (offAction != null) { + offAction.execute(); + } + } + } + + @Override + public String getDescription() { + if (toggled) { + String onDesc = onAction != null ? onAction.getDescription() : "No action"; + return "Toggle ON: " + onDesc; + } else { + String offDesc = offAction != null ? offAction.getDescription() : "No action"; + return "Toggle OFF: " + offDesc; + } + } + + /** + * Checks if this action is toggled on. + * + * @return True if toggled on, false if toggled off + */ + public boolean isToggled() { + return toggled; + } + + /** + * Sets the toggled state. + * + * @param toggled The new toggled state + */ + public void setToggled(boolean toggled) { + this.toggled = toggled; + } + + /** + * Gets the action executed when toggled off. + * + * @return The off action + */ + public Action getOffAction() { + return offAction; + } + + /** + * Sets the action to execute when toggled off. + * + * @param offAction The new off action + */ + public void setOffAction(Action offAction) { + this.offAction = offAction; + } + + /** + * Gets the action executed when toggled on. + * + * @return The on action + */ + public Action getOnAction() { + return onAction; + } + + /** + * Sets the action to execute when toggled on. + * + * @param onAction The new on action + */ + public void setOnAction(Action onAction) { + this.onAction = onAction; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/core/Game.java b/app/src/main/java/com/niravramdhanie/twod/game/core/Game.java similarity index 97% rename from app/src/main/java/com/vincentramdhanie/twod/game/core/Game.java rename to app/src/main/java/com/niravramdhanie/twod/game/core/Game.java index 8df1146..9e32cb7 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/core/Game.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/core/Game.java @@ -1,12 +1,12 @@ -package com.vincentramdhanie.twod.game.core; - -import com.vincentramdhanie.twod.game.input.KeyHandler; -import com.vincentramdhanie.twod.game.input.MouseHandler; +package com.niravramdhanie.twod.game.core; import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.Timer; +import com.niravramdhanie.twod.game.input.KeyHandler; +import com.niravramdhanie.twod.game.input.MouseHandler; + public class Game implements Runnable { private JFrame window; private GamePanel gamePanel; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/core/GamePanel.java b/app/src/main/java/com/niravramdhanie/twod/game/core/GamePanel.java similarity index 99% rename from app/src/main/java/com/vincentramdhanie/twod/game/core/GamePanel.java rename to app/src/main/java/com/niravramdhanie/twod/game/core/GamePanel.java index 8100acc..9b30bbd 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/core/GamePanel.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/core/GamePanel.java @@ -1,16 +1,17 @@ -package com.vincentramdhanie.twod.game.core; +package com.niravramdhanie.twod.game.core; -import javax.swing.JPanel; +import java.awt.Color; import java.awt.Dimension; +import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; -import java.awt.Color; -import java.awt.Font; -import java.awt.RenderingHints; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; +import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import javax.swing.JPanel; + public class GamePanel extends JPanel { private BufferedImage image; private Graphics2D g2d; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/core/GameStateManager.java b/app/src/main/java/com/niravramdhanie/twod/game/core/GameStateManager.java similarity index 94% rename from app/src/main/java/com/vincentramdhanie/twod/game/core/GameStateManager.java rename to app/src/main/java/com/niravramdhanie/twod/game/core/GameStateManager.java index 1bcd43b..c8c6f29 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/core/GameStateManager.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/core/GameStateManager.java @@ -1,16 +1,16 @@ -package com.vincentramdhanie.twod.game.core; - -import com.vincentramdhanie.twod.game.state.GameState; -import com.vincentramdhanie.twod.game.state.MenuState; -import com.vincentramdhanie.twod.game.state.PlayState; -import com.vincentramdhanie.twod.game.state.PauseState; -import com.vincentramdhanie.twod.game.input.KeyHandler; -import com.vincentramdhanie.twod.game.input.MouseHandler; +package com.niravramdhanie.twod.game.core; import java.awt.Graphics2D; import java.util.ArrayList; import java.util.List; +import com.niravramdhanie.twod.game.input.KeyHandler; +import com.niravramdhanie.twod.game.input.MouseHandler; +import com.niravramdhanie.twod.game.state.GameState; +import com.niravramdhanie.twod.game.state.MenuState; +import com.niravramdhanie.twod.game.state.PauseState; +import com.niravramdhanie.twod.game.state.PlayState; + public class GameStateManager { private List gameStates; private int currentState; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/entity/BallPlayer.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/BallPlayer.java similarity index 54% rename from app/src/main/java/com/vincentramdhanie/twod/game/entity/BallPlayer.java rename to app/src/main/java/com/niravramdhanie/twod/game/entity/BallPlayer.java index 1228ba8..ad0671a 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/entity/BallPlayer.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/BallPlayer.java @@ -1,14 +1,14 @@ -package com.vincentramdhanie.twod.game.entity; +package com.niravramdhanie.twod.game.entity; -import com.vincentramdhanie.twod.game.graphics.Animation; -import com.vincentramdhanie.twod.game.graphics.SpriteSheet; -import com.vincentramdhanie.twod.game.utils.ResourceLoader; - -import java.awt.Graphics2D; import java.awt.Color; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.List; +import com.niravramdhanie.twod.game.graphics.Animation; +import com.niravramdhanie.twod.game.graphics.SpriteSheet; +import com.niravramdhanie.twod.game.utils.ResourceLoader; + public class BallPlayer extends Entity { // Movement flags private boolean left; @@ -19,7 +19,6 @@ public class BallPlayer extends Entity { // Movement properties private float moveSpeed; private float maxSpeed; - private float friction; // Health properties private int health; @@ -41,15 +40,18 @@ public class BallPlayer extends Entity { private int screenHeight; private List blocks; + // Box carrying + private Box carriedBox; + public BallPlayer(float x, float y, int width, int height, int screenWidth, int screenHeight) { super(x, y, width, height); this.screenWidth = screenWidth; this.screenHeight = screenHeight; + this.carriedBox = null; - // Set movement properties - moveSpeed = 0.5f; - maxSpeed = 5.0f; - friction = 0.1f; + // Set movement properties - instant movement with no friction + moveSpeed = 0.84f; // 1.2f * 0.7 = 0.84f + maxSpeed = 2.8f; // 4.0f * 0.7 = 2.8f // Set health properties this.maxHealth = 100; @@ -124,36 +126,76 @@ public void setBlocks(List blocks) { this.blocks = blocks; } + /** + * Sets the box being carried by the player. + * + * @param box The box being carried, or null if not carrying any box + */ + public void setCarriedBox(Box box) { + this.carriedBox = box; + } + + /** + * Gets the box being carried by the player. + * + * @return The box being carried, or null if not carrying any box + */ + public Box getCarriedBox() { + return carriedBox; + } + + /** + * Checks if the player is carrying a box. + * + * @return True if carrying a box, false otherwise + */ + public boolean isCarryingBox() { + return carriedBox != null; + } + @Override public void update() { // Track previous animation for transition checks Animation previousAnim = currentAnim; - // Handle movement - // Apply acceleration based on input - if (left) { - velocity.x -= moveSpeed; - if (velocity.x < -maxSpeed) velocity.x = -maxSpeed; - if (spritesLoaded) currentAnim = leftAnim; - } - if (right) { - velocity.x += moveSpeed; - if (velocity.x > maxSpeed) velocity.x = maxSpeed; - if (spritesLoaded) currentAnim = rightAnim; + // Reset velocity immediately when no keys are pressed for instant stopping + if (!left && !right) velocity.x = 0; + if (!up && !down) velocity.y = 0; + + // Calculate input direction vector + float dirX = 0, dirY = 0; + if (left) dirX -= 1; + if (right) dirX += 1; + if (up) dirY -= 1; + if (down) dirY += 1; + + // Normalize diagonal movement + if (dirX != 0 && dirY != 0) { + // Calculate the normalized direction + float length = (float)Math.sqrt(dirX * dirX + dirY * dirY); + dirX /= length; + dirY /= length; } - if (up) { - velocity.y -= moveSpeed; - if (velocity.y < -maxSpeed) velocity.y = -maxSpeed; - if (spritesLoaded) currentAnim = upAnim; + + // Apply movement only if keys are pressed (no acceleration buildup) + if (dirX != 0) { + velocity.x = dirX * maxSpeed; } - if (down) { - velocity.y += moveSpeed; - if (velocity.y > maxSpeed) velocity.y = maxSpeed; - if (spritesLoaded) currentAnim = downAnim; + + if (dirY != 0) { + velocity.y = dirY * maxSpeed; } - // If no movement keys are pressed, use idle animation - if (!left && !right && !up && !down) { + // Set appropriate animation based on movement direction + if (velocity.x < 0 && Math.abs(velocity.x) > Math.abs(velocity.y)) { + if (spritesLoaded) currentAnim = leftAnim; + } else if (velocity.x > 0 && Math.abs(velocity.x) > Math.abs(velocity.y)) { + if (spritesLoaded) currentAnim = rightAnim; + } else if (velocity.y < 0 && Math.abs(velocity.y) > Math.abs(velocity.x)) { + if (spritesLoaded) currentAnim = upAnim; + } else if (velocity.y > 0 && Math.abs(velocity.y) > Math.abs(velocity.x)) { + if (spritesLoaded) currentAnim = downAnim; + } else if (velocity.x == 0 && velocity.y == 0) { if (spritesLoaded) currentAnim = idleAnim; } @@ -162,27 +204,6 @@ public void update() { currentAnim.reset(); } - // Apply friction - if (!left && !right) { - if (velocity.x > 0) { - velocity.x -= friction; - if (velocity.x < 0) velocity.x = 0; - } else if (velocity.x < 0) { - velocity.x += friction; - if (velocity.x > 0) velocity.x = 0; - } - } - - if (!up && !down) { - if (velocity.y > 0) { - velocity.y -= friction; - if (velocity.y < 0) velocity.y = 0; - } else if (velocity.y < 0) { - velocity.y += friction; - if (velocity.y > 0) velocity.y = 0; - } - } - // Calculate new position float newX = position.x + velocity.x; float newY = position.y + velocity.y; @@ -196,35 +217,21 @@ public void update() { // Check block collisions for X movement boolean collisionX = false; position.x = newX; - for (Block block : blocks) { - if (checkCollision(block)) { - collisionX = true; - // Resolve X collision - if (velocity.x > 0) { // Moving right - position.x = block.getX() - width; - } else if (velocity.x < 0) { // Moving left - position.x = block.getX() + block.getWidth(); - } - velocity.x = 0; - break; - } + + if (checkBlockCollisionX()) { + collisionX = true; + // Position already corrected in checkBlockCollisionX() + velocity.x = 0; } // Check block collisions for Y movement boolean collisionY = false; position.y = newY; - for (Block block : blocks) { - if (checkCollision(block)) { - collisionY = true; - // Resolve Y collision - if (velocity.y > 0) { // Moving down - position.y = block.getY() - height; - } else if (velocity.y < 0) { // Moving up - position.y = block.getY() + block.getHeight(); - } - velocity.y = 0; - break; - } + + if (checkBlockCollisionY()) { + collisionY = true; + // Position already corrected in checkBlockCollisionY() + velocity.y = 0; } // If no collision occurred, move normally @@ -304,4 +311,134 @@ public boolean isAlive() { public void setRight(boolean right) { this.right = right; } public void setUp(boolean up) { this.up = up; } public void setDown(boolean down) { this.down = down; } + + /** + * Gets the player's velocity vector. + * + * @return The velocity vector + */ + public com.niravramdhanie.twod.game.utils.Vector2D getVelocity() { + return velocity; + } + + /** + * Checks for X-axis collisions with blocks. + * When carrying a box, the box acts as an extension of the player's hitbox. + * + * @return True if a collision occurred, false otherwise + */ + private boolean checkBlockCollisionX() { + // First check player collision with blocks (but not with the carried box) + for (Block block : blocks) { + if (block != carriedBox && checkCollision(block)) { + // Resolve X collision + if (velocity.x > 0) { // Moving right + position.x = block.getX() - width; + } else if (velocity.x < 0) { // Moving left + position.x = block.getX() + block.getWidth(); + } + velocity.x = 0; + return true; + } + } + + // If carrying a box, check if the box would collide with any blocks + if (carriedBox != null && velocity.x != 0) { + // Calculate where the box would be after the player's movement + float boxX = position.x + width/2 - carriedBox.getWidth()/2 + carriedBox.getRelativeX(); + float boxY = position.y + height/2 - carriedBox.getHeight()/2 + carriedBox.getRelativeY(); + + // Create a rectangle representing the box's position + java.awt.Rectangle boxBounds = new java.awt.Rectangle( + (int)boxX, (int)boxY, carriedBox.getWidth(), carriedBox.getHeight() + ); + + // Check for collisions with any block + for (Block block : blocks) { + if (block != carriedBox) { + java.awt.Rectangle blockBounds = block.getBounds(); + + if (boxBounds.intersects(blockBounds)) { + // Resolve collision just like we would for the player + if (velocity.x > 0) { // Moving right + position.x = block.getX() - width - (boxX + carriedBox.getWidth() - position.x - width); + } else if (velocity.x < 0) { // Moving left + position.x = block.getX() + block.getWidth() - (boxX - position.x); + } + velocity.x = 0; + return true; + } + } + } + } + + return false; + } + + /** + * Checks for Y-axis collisions with blocks. + * When carrying a box, the box acts as an extension of the player's hitbox. + * + * @return True if a collision occurred, false otherwise + */ + private boolean checkBlockCollisionY() { + // First check player collision with blocks (but not with the carried box) + for (Block block : blocks) { + if (block != carriedBox && checkCollision(block)) { + // Resolve Y collision + if (velocity.y > 0) { // Moving down + position.y = block.getY() - height; + } else if (velocity.y < 0) { // Moving up + position.y = block.getY() + block.getHeight(); + } + velocity.y = 0; + return true; + } + } + + // If carrying a box, check if the box would collide with any blocks + if (carriedBox != null && velocity.y != 0) { + // Calculate where the box would be after the player's movement + float boxX = position.x + width/2 - carriedBox.getWidth()/2 + carriedBox.getRelativeX(); + float boxY = position.y + height/2 - carriedBox.getHeight()/2 + carriedBox.getRelativeY(); + + // Create a rectangle representing the box's position + java.awt.Rectangle boxBounds = new java.awt.Rectangle( + (int)boxX, (int)boxY, carriedBox.getWidth(), carriedBox.getHeight() + ); + + // Check for collisions with any block + for (Block block : blocks) { + if (block != carriedBox) { + java.awt.Rectangle blockBounds = block.getBounds(); + + if (boxBounds.intersects(blockBounds)) { + // Resolve collision just like we would for the player + if (velocity.y > 0) { // Moving down + position.y = block.getY() - height - (boxY + carriedBox.getHeight() - position.y - height); + } else if (velocity.y < 0) { // Moving up + position.y = block.getY() + block.getHeight() - (boxY - position.y); + } + velocity.y = 0; + return true; + } + } + } + } + + return false; + } + + /** + * Override collision detection to disable collision with the carried box + */ + @Override + public boolean checkCollision(Entity other) { + // Don't collide with the box we're carrying + if (carriedBox != null && other == carriedBox) { + return false; + } + // Use standard collision detection for everything else + return super.checkCollision(other); + } } \ No newline at end of file diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/entity/Block.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/Block.java similarity index 93% rename from app/src/main/java/com/vincentramdhanie/twod/game/entity/Block.java rename to app/src/main/java/com/niravramdhanie/twod/game/entity/Block.java index eb04e99..3cfb651 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/entity/Block.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/Block.java @@ -1,9 +1,10 @@ -package com.vincentramdhanie.twod.game.entity; +package com.niravramdhanie.twod.game.entity; -import java.awt.Graphics2D; import java.awt.Color; +import java.awt.Graphics2D; import java.awt.image.BufferedImage; -import com.vincentramdhanie.twod.game.utils.ResourceLoader; + +import com.niravramdhanie.twod.game.utils.ResourceLoader; public class Block extends Entity { private BufferedImage blockImage; diff --git a/app/src/main/java/com/niravramdhanie/twod/game/entity/Box.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/Box.java new file mode 100644 index 0000000..390a411 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/Box.java @@ -0,0 +1,370 @@ +package com.niravramdhanie.twod.game.entity; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import com.niravramdhanie.twod.game.utils.ResourceLoader; + +/** + * A box entity that can be picked up and carried by the player. + * The box can be in an active or inactive state, where the active state + * means it will follow the rewind system. + */ +public class Box extends Block { + // Movement tracking + private boolean isBeingCarried; + private float relativeX; // Relative position to player when picked up + private float relativeY; + + // Box properties + private boolean isActive; // Whether the box follows the rewind system + private boolean isMovable; // Whether the box can be picked up + + // Visual properties + private Color boxColor; + private Color activeBoxColor; + private BufferedImage boxImage; + private BufferedImage activeBoxImage; + + // Reference to the carrier (player that is carrying this box) + private Entity carrier; + + /** + * Creates a new box entity. + * + * @param x The x position + * @param y The y position + * @param width The width + * @param height The height + * @param isActive Whether the box follows the rewind system + * @param isMovable Whether the box can be picked up + */ + public Box(float x, float y, int width, int height, boolean isActive, boolean isMovable) { + super(x, y, width, height); + this.isBeingCarried = false; + this.isActive = isActive; + this.isMovable = isMovable; + this.carrier = null; + + // Default colors + this.boxColor = new Color(139, 69, 19); // Brown + this.activeBoxColor = new Color(205, 133, 63); // Peru (lighter brown) + + try { + // Load box images (fallback to colors if images can't be loaded) + boxImage = ResourceLoader.loadImage("/sprites/box.png"); + activeBoxImage = ResourceLoader.loadImage("/sprites/box_active.png"); + } catch (Exception e) { + System.err.println("Error loading box images: " + e.getMessage()); + boxImage = null; + activeBoxImage = null; + } + } + + @Override + public void update() { + // Box behavior is mainly handled in PlayState + } + + /** + * Override collision detection to disable collision when being carried or when colliding with the carrier + */ + @Override + public boolean checkCollision(Entity other) { + // If being carried, don't register any collisions at all with the carrier + if (isBeingCarried && (carrier == other || other == carrier)) { + return false; + } + // Otherwise, use the standard collision detection + return super.checkCollision(other); + } + + @Override + public void render(Graphics2D g) { + // Use images if available, otherwise draw with colors + if ((isActive && activeBoxImage != null) || (!isActive && boxImage != null)) { + g.drawImage(isActive ? activeBoxImage : boxImage, + (int)position.x, (int)position.y, width, height, null); + } else { + // Fallback to drawing with colors + drawBox(g); + } + } + + /** + * Draws the box with gradients and highlights for a 3D effect + */ + private void drawBox(Graphics2D g) { + // Save original paint + java.awt.Paint originalPaint = g.getPaint(); + + // Choose appropriate color based on state + Color baseColor = isActive ? activeBoxColor : boxColor; + Color topColor = baseColor.brighter(); + Color bottomColor = baseColor.darker(); + + // Create gradient for 3D effect + GradientPaint gradient = new GradientPaint( + (int)position.x, (int)position.y, topColor, + (int)position.x, (int)position.y + height, bottomColor + ); + + // Apply gradient and draw box + g.setPaint(gradient); + g.fillRect((int)position.x, (int)position.y, width, height); + + // Draw box edges (darker) + g.setColor(bottomColor.darker()); + g.drawRect((int)position.x, (int)position.y, width, height); + + // Draw highlights + int highlight = 4; + int shadow = 4; + + // Top highlight + g.setColor(new Color(255, 255, 255, 100)); + g.fillRect((int)position.x + 1, (int)position.y + 1, width - 2, highlight); + + // Left highlight + g.fillRect((int)position.x + 1, (int)position.y + highlight + 1, + highlight, height - highlight - shadow - 1); + + // Bottom shadow + g.setColor(new Color(0, 0, 0, 80)); + g.fillRect((int)position.x + 1, (int)position.y + height - shadow, + width - 2, shadow - 1); + + // Right shadow + g.fillRect((int)position.x + width - shadow, (int)position.y + highlight + 1, + shadow - 1, height - highlight - shadow - 1); + + // Draw a small indicator if the box is active + if (isActive) { + int indicatorSize = 6; + g.setColor(new Color(0, 255, 0, 180)); + g.fillOval((int)position.x + width - indicatorSize - 2, + (int)position.y + 2, indicatorSize, indicatorSize); + } + + // Restore original paint + g.setPaint(originalPaint); + } + + /** + * Picks up the box, storing its relative position to the player. + * + * @param playerX The player's X position + * @param playerY The player's Y position + * @param carrier The entity carrying this box + * @return True if the box was picked up, false if it can't be picked up + */ + public boolean pickUp(float playerX, float playerY, Entity carrier) { + if (!isMovable || isBeingCarried) { + return false; + } + + // Store relative position to the player (center to center) + float playerCenterX = playerX + carrier.getWidth() / 2; + float playerCenterY = playerY + carrier.getHeight() / 2; + float boxCenterX = position.x + width / 2; + float boxCenterY = position.y + height / 2; + + // Calculate offset from player center to box center + // This preserves the exact relative position at pickup moment + relativeX = boxCenterX - playerCenterX; + relativeY = boxCenterY - playerCenterY; + + // Log the pickup for debugging + System.out.println("Box picked up with relative position: " + relativeX + ", " + relativeY); + System.out.println("Player center: " + playerCenterX + ", " + playerCenterY); + System.out.println("Box center: " + boxCenterX + ", " + boxCenterY); + + isBeingCarried = true; + this.carrier = carrier; + + return true; + } + + /** + * Picks up the box without a specific carrier reference. + * + * @param playerX The player's X position + * @param playerY The player's Y position + * @return True if the box was picked up, false if it can't be picked up + */ + public boolean pickUp(float playerX, float playerY) { + return pickUp(playerX, playerY, null); + } + + /** + * Updates the box position when being carried by the player. + * + * @param playerX The player's X position + * @param playerY The player's Y position + */ + public void updateCarriedPosition(float playerX, float playerY) { + if (!isBeingCarried) return; + + if (carrier != null) { + // Calculate position using the current player position and the relative offset + float playerCenterX = playerX + carrier.getWidth() / 2; + float playerCenterY = playerY + carrier.getHeight() / 2; + + // Get player velocity if available (for smoother movement) + float playerVelX = 0; + float playerVelY = 0; + if (carrier instanceof BallPlayer) { + playerVelX = ((BallPlayer)carrier).getVelocity().x; + playerVelY = ((BallPlayer)carrier).getVelocity().y; + } + + // Position the box with its center at the correct offset from player center + // Maintain the original relative position but adjust for any movement direction + float boxCenterX = playerCenterX + relativeX; + float boxCenterY = playerCenterY + relativeY; + + // Convert back to top-left corner position + position.x = boxCenterX - width / 2; + position.y = boxCenterY - height / 2; + } else { + // Fallback if carrier is null + position.x = playerX + relativeX; + position.y = playerY + relativeY; + } + } + + /** + * Drops the box at its current position. + */ + public void drop() { + isBeingCarried = false; + carrier = null; + + // Ensure the box is positioned on the grid when dropped + // This helps prevent the box from being placed at weird positions + if (carrier instanceof BallPlayer) { + // Snap to grid if needed - uncomment if you want this behavior + /* + int gridCellSize = 32; // Should match GRID_CELL_SIZE in PlayState + float gridX = Math.round(position.x / gridCellSize) * gridCellSize; + float gridY = Math.round(position.y / gridCellSize) * gridCellSize; + position.x = gridX; + position.y = gridY; + */ + } + + System.out.println("Box dropped at position: " + position.x + ", " + position.y); + } + + /** + * Checks if the box is being carried. + * + * @return True if being carried, false otherwise + */ + public boolean isBeingCarried() { + return isBeingCarried; + } + + /** + * Gets the entity that is carrying this box. + * + * @return The entity carrying this box, or null if not being carried + */ + public Entity getCarrier() { + return carrier; + } + + /** + * Checks if the box is in an active state (follows rewind). + * + * @return True if active, false otherwise + */ + public boolean isActive() { + return isActive; + } + + /** + * Sets whether the box is in an active state. + * + * @param isActive Whether the box should follow the rewind system + */ + public void setActive(boolean isActive) { + this.isActive = isActive; + } + + /** + * Checks if the box is movable. + * + * @return True if the box can be picked up, false otherwise + */ + public boolean isMovable() { + return isMovable; + } + + /** + * Sets whether the box is movable. + * + * @param isMovable Whether the box can be picked up + */ + public void setMovable(boolean isMovable) { + this.isMovable = isMovable; + } + + /** + * Overrides position setter to handle the carried state + */ + @Override + public void setX(float x) { + if (!isBeingCarried) { + super.setX(x); + } + } + + /** + * Overrides position setter to handle the carried state + */ + @Override + public void setY(float y) { + if (!isBeingCarried) { + super.setY(y); + } + } + + /** + * Gets the relative X position to the carrier when picked up. + * + * @return The relative X position + */ + public float getRelativeX() { + return relativeX; + } + + /** + * Gets the relative Y position to the carrier when picked up. + * + * @return The relative Y position + */ + public float getRelativeY() { + return relativeY; + } + + /** + * Sets the relative X position to the carrier. + * + * @param relativeX The relative X position + */ + public void setRelativeX(float relativeX) { + this.relativeX = relativeX; + } + + /** + * Sets the relative Y position to the carrier. + * + * @param relativeY The relative Y position + */ + public void setRelativeY(float relativeY) { + this.relativeY = relativeY; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/entity/Button.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/Button.java new file mode 100644 index 0000000..36b4e1e --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/Button.java @@ -0,0 +1,564 @@ +package com.niravramdhanie.twod.game.entity; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; + +import com.niravramdhanie.twod.game.actions.Action; +import com.niravramdhanie.twod.game.actions.DoorAction; +import com.niravramdhanie.twod.game.actions.MessageAction; +import com.niravramdhanie.twod.game.actions.MultiAction; +import com.niravramdhanie.twod.game.actions.TimedAction; +import com.niravramdhanie.twod.game.actions.ToggleAction; +import com.niravramdhanie.twod.game.utils.ResourceLoader; +import com.niravramdhanie.twod.game.utils.RewindManager; + +/** + * A button entity that can be placed on the grid and activated by the player. + * The button is activated when the player is one grid space away and presses 'E'. + */ +public class Button extends Entity { + private BufferedImage buttonImage; + private BufferedImage buttonActiveImage; + private Color color; + private Color activeColor; + private boolean activated; + private Action action; + + // Fonts for button icons + private static Font iconFont; + + // Reference to the rewind manager + private static RewindManager rewindManager; + + // Visual effects for activation + private long activationTime; + private boolean pulseEffect; + + static { + // Initialize the icon font + iconFont = new Font("Arial", Font.BOLD, 14); + } + + /** + * Creates a new button entity with the specified action. + * + * @param x The x position + * @param y The y position + * @param width The width + * @param height The height + * @param action The action to execute when the button is activated + */ + public Button(float x, float y, int width, int height, Action action) { + super(x, y, width, height); + this.action = action; + this.activated = false; + this.pulseEffect = true; + this.activationTime = 0; + + // Default colors + this.color = new Color(200, 50, 50); // Red when inactive + this.activeColor = new Color(50, 200, 50); // Green when active + + try { + // Load button images + buttonImage = ResourceLoader.loadImage("/sprites/button.png"); + buttonActiveImage = ResourceLoader.loadImage("/sprites/button_active.png"); + } catch (Exception e) { + System.err.println("Error loading button images: " + e.getMessage()); + buttonImage = null; + buttonActiveImage = null; + } + + // If this is a timed action, set the target button + if (action instanceof TimedAction) { + ((TimedAction) action).setTargetButton(this); + } + } + + /** + * Creates a new button entity with no action. + * + * @param x The x position + * @param y The y position + * @param width The width + * @param height The height + */ + public Button(float x, float y, int width, int height) { + this(x, y, width, height, null); + } + + @Override + public void update() { + // Update activation time for effects + if (activated) { + if (activationTime == 0) { + activationTime = System.currentTimeMillis(); + } + } else { + activationTime = 0; + } + } + + @Override + public void render(Graphics2D g) { + try { + // Draw the base button + drawButtonBase(g); + + // Draw action-specific icon + drawButtonIcon(g); + + // Draw additional indicators based on action type + if (action != null) { + // For timed actions, draw a timer when activated + if (action instanceof TimedAction && activated) { + drawTimerBar(g); + } + } + + // Draw activation glow if activated + if (activated) { + drawActivationIndicator(g); + } + } catch (Exception e) { + // Ultimate fallback + System.err.println("Error rendering button: " + e.getMessage()); + g.setColor(Color.MAGENTA); + g.fillRect((int)position.x, (int)position.y, width, height); + } + } + + /** + * Draws the base button shape with appropriate colors + */ + private void drawButtonBase(Graphics2D g) { + // Determine colors based on action type and state + Color baseColor = determineButtonColor(); + Color topColor = baseColor.brighter(); + Color bottomColor = baseColor.darker(); + + // Create gradient for 3D effect + GradientPaint gradient = new GradientPaint( + (int)position.x, (int)position.y, topColor, + (int)position.x, (int)position.y + height, bottomColor + ); + + // Store original paint and set gradient + java.awt.Paint originalPaint = g.getPaint(); + g.setPaint(gradient); + + // Draw button body with rounded corners + g.fillRoundRect((int)position.x, (int)position.y, width, height, 5, 5); + + // Draw button border + g.setColor(activated ? Color.WHITE : Color.DARK_GRAY); + g.drawRoundRect((int)position.x, (int)position.y, width, height, 5, 5); + + // Draw button top (lighter) for 3D effect + g.setColor(new Color(255, 255, 255, 70)); + g.fillRoundRect((int)position.x + 2, (int)position.y + 2, width - 4, height / 3, 3, 3); + + // Reset original paint + g.setPaint(originalPaint); + } + + /** + * Determines the appropriate color for the button based on its action type and state + */ + private Color determineButtonColor() { + if (action == null) { + return activated ? activeColor : color; + } + + // Colors for different action types + if (action instanceof TimedAction) { + return activated ? new Color(255, 140, 0) : new Color(200, 110, 0); // Orange for timed + } else if (action instanceof ToggleAction) { + ToggleAction toggleAction = (ToggleAction) action; + if (toggleAction.isToggled()) { + return new Color(0, 170, 220); // Blue when toggled on + } else { + return new Color(60, 60, 180); // Dark blue when toggled off + } + } else if (action instanceof MessageAction) { + MessageAction messageAction = (MessageAction) action; + if (messageAction.isCycling()) { + return activated ? new Color(180, 100, 200) : new Color(120, 60, 140); // Purple for cycling + } else { + return activated ? new Color(220, 220, 50) : new Color(180, 180, 40); // Yellow for regular + } + } else if (action instanceof DoorAction) { + return activated ? new Color(50, 200, 50) : new Color(40, 130, 40); // Green for door + } else if (action instanceof MultiAction) { + return activated ? new Color(200, 50, 200) : new Color(150, 30, 150); // Magenta for multi + } + + return activated ? activeColor : color; + } + + /** + * Draws an appropriate icon based on the button's action type + */ + private void drawButtonIcon(Graphics2D g) { + if (action == null) { + return; + } + + // Save original font and color + Font originalFont = g.getFont(); + Color originalColor = g.getColor(); + + // Set icon font + g.setFont(iconFont); + + // Get icon and color based on action type + String icon = ""; + Color iconColor = Color.WHITE; + + if (action instanceof TimedAction) { + icon = "⏱"; // Clock icon for timed actions + iconColor = Color.WHITE; + } else if (action instanceof ToggleAction) { + icon = "⇆"; // Toggle icon + iconColor = Color.WHITE; + + ToggleAction toggleAction = (ToggleAction) action; + if (toggleAction.isToggled()) { + icon = "ON"; + } else { + icon = "OFF"; + } + + } else if (action instanceof MessageAction) { + MessageAction messageAction = (MessageAction) action; + if (messageAction.isCycling()) { + icon = "♻"; // Recycling symbol for cycling messages + } else { + icon = "!"; // Exclamation for regular messages + } + iconColor = Color.WHITE; + } else if (action instanceof DoorAction) { + icon = "🚪"; // Door symbol + iconColor = Color.WHITE; + } else if (action instanceof MultiAction) { + icon = "✦"; // Star for multi-actions + iconColor = Color.WHITE; + } + + // Center the icon in the button + FontMetrics metrics = g.getFontMetrics(); + Rectangle2D bounds = metrics.getStringBounds(icon, g); + int x = (int)(position.x + (width - bounds.getWidth()) / 2); + int y = (int)(position.y + (height - bounds.getHeight()) / 2 + metrics.getAscent()); + + // Draw icon with a subtle shadow for better visibility + g.setColor(Color.BLACK); + g.drawString(icon, x + 1, y + 1); + g.setColor(iconColor); + g.drawString(icon, x, y); + + // Draw interaction hint + drawInteractionHint(g); + + // Restore original font and color + g.setFont(originalFont); + g.setColor(originalColor); + } + + /** + * Draws a small "E" hint to show the button can be activated + */ + private void drawInteractionHint(Graphics2D g) { + g.setFont(new Font("Arial", Font.BOLD, 10)); + g.setColor(new Color(255, 255, 255, 200)); + g.drawString("E", (int)position.x + width - 10, (int)position.y + 12); + } + + /** + * Draws a timer bar for timed actions + */ + private void drawTimerBar(Graphics2D g) { + if (!(action instanceof TimedAction)) return; + + TimedAction timedAction = (TimedAction) action; + if (!timedAction.isActive()) return; + + // Store original color + Color originalColor = g.getColor(); + + // Get the timer fraction (0.0 to 1.0) + float fraction = timedAction.getTimeRemainingFraction(); + + // Draw the timer bar background + g.setColor(Color.DARK_GRAY); + g.fillRect((int)position.x, (int)position.y + height + 2, width, 4); + + // Draw the timer bar foreground + if (fraction > 0.6f) { + g.setColor(Color.GREEN); + } else if (fraction > 0.3f) { + g.setColor(Color.YELLOW); + } else { + g.setColor(Color.RED); + } + + g.fillRect((int)position.x, (int)position.y + height + 2, (int)(width * fraction), 4); + + // Restore original color + g.setColor(originalColor); + } + + /** + * Draws a toggle indicator for toggle actions. + * + * @param g The graphics context + * @param toggled Whether the action is toggled on + */ + private void drawToggleIndicator(Graphics2D g, boolean toggled) { + g.setColor(Color.WHITE); + int circleSize = Math.min(width, height) / 3; + int circleX = (int)position.x + width - circleSize - 5; + int circleY = (int)position.y + 5; + + g.drawOval(circleX, circleY, circleSize, circleSize); + + if (toggled) { + g.fillOval(circleX, circleY, circleSize, circleSize); + } + } + + /** + * Draws an indicator for message actions. + * + * @param g The graphics context + * @param messageAction The message action + */ + private void drawMessageIndicator(Graphics2D g, MessageAction messageAction) { + int indicatorSize = Math.min(width, height) / 4; + int indicatorX = (int)position.x + width - indicatorSize - 5; + int indicatorY = (int)position.y + height - indicatorSize - 5; + + if (messageAction.isCycling()) { + // Draw cycling indicator (circular arrow) + g.setColor(Color.WHITE); + g.drawOval(indicatorX, indicatorY, indicatorSize, indicatorSize); + + // Draw arrow inside the circle + int centerX = indicatorX + indicatorSize / 2; + int centerY = indicatorY + indicatorSize / 2; + int arrowSize = indicatorSize / 3; + + g.drawLine(centerX, centerY - arrowSize, centerX + arrowSize, centerY); + g.drawLine(centerX, centerY - arrowSize, centerX - arrowSize, centerY); + } else { + // Draw speech bubble for regular messages + g.setColor(Color.WHITE); + g.fillRoundRect(indicatorX, indicatorY, indicatorSize, indicatorSize, 3, 3); + g.setColor(Color.BLACK); + g.drawString("!", indicatorX + indicatorSize/3, indicatorY + 2*indicatorSize/3); + } + } + + /** + * Draws a visual indicator that the button is activated + */ + private void drawActivationIndicator(Graphics2D g) { + // Store original color + Color originalColor = g.getColor(); + + // Create a pulsating effect + float alpha = 0.7f; + if (pulseEffect) { + long elapsed = System.currentTimeMillis() - activationTime; + alpha = 0.3f + (float)Math.abs(Math.sin(elapsed * 0.005f)) * 0.4f; + } + + // Draw a glowing outline + g.setColor(new Color( + activeColor.getRed() / 255f, + activeColor.getGreen() / 255f, + activeColor.getBlue() / 255f, + alpha + )); + + // Draw outer glow + int glowSize = 4; + g.fillRoundRect( + (int)position.x - glowSize, + (int)position.y - glowSize, + width + glowSize * 2, + height + glowSize * 2, + 10, 10 + ); + + // Restore original color + g.setColor(originalColor); + } + + /** + * Sets the rewind manager for all buttons. + * + * @param rewindManager The rewind manager + */ + public static void setRewindManager(RewindManager rewindManager) { + Button.rewindManager = rewindManager; + } + + /** + * Activates the button and executes its action. + * + * @return True if the button was activated, false if it was already activated + */ + public boolean activate() { + boolean wasActivated = activated; + + if (activated) { + // If already activated, only perform action if it's a toggle or cycling action + if (action instanceof ToggleAction || + (action instanceof MessageAction && ((MessageAction)action).isCycling())) { + if (action != null) { + action.execute(); + } + + // Record the button activation if we're recording + if (rewindManager != null) { + rewindManager.recordButtonActivation(this, true); + } + + return true; + } + return false; + } + + activated = true; + if (action != null) { + action.execute(); + } + + // Record the button activation if we're recording + if (rewindManager != null && !wasActivated) { + rewindManager.recordButtonActivation(this, true); + } + + return true; + } + + /** + * Deactivates the button. + */ + public void deactivate() { + boolean wasActivated = activated; + activated = false; + + // Record the button deactivation if we're recording + if (rewindManager != null && wasActivated) { + rewindManager.recordButtonActivation(this, false); + } + } + + /** + * Checks if the button is activated. + * + * @return True if activated, false otherwise + */ + public boolean isActivated() { + return activated; + } + + /** + * Sets the action to execute when the button is activated. + * + * @param action The action + */ + public void setAction(Action action) { + this.action = action; + + // If this is a timed action, set the target button + if (action instanceof TimedAction) { + ((TimedAction) action).setTargetButton(this); + } + } + + /** + * Gets the button's action. + * + * @return The action + */ + public Action getAction() { + return action; + } + + /** + * Sets the button's inactive color. + * + * @param color The color + */ + public void setColor(Color color) { + this.color = color; + } + + /** + * Sets the button's active color. + * + * @param activeColor The active color + */ + public void setActiveColor(Color activeColor) { + this.activeColor = activeColor; + } + + /** + * Force sets the toggle state without executing any actions. + * Useful for rewind operations where we want to set the state + * without triggering the actual toggle behavior. + * + * @param toggled The toggle state to set + * @return True if the action was a ToggleAction and state was set, false otherwise + */ + public boolean forceSetToggleState(boolean toggled) { + if (action instanceof ToggleAction) { + ToggleAction toggleAction = (ToggleAction) action; + toggleAction.setToggled(toggled); + + // If this toggle controls a door, update the door state directly + if (toggleAction.getOnAction() instanceof DoorAction) { + DoorAction doorAction = (DoorAction) toggleAction.getOnAction(); + doorAction.setDoorOpen(toggled); + } + + return true; + } + return false; + } + + /** + * Sets whether the button is activated without executing any actions. + * Useful for rewind operations. + * + * @param activated Whether the button should be activated + */ + public void setActivated(boolean activated) { + this.activated = activated; + + // Update activation time when the button is activated during rewind + // to ensure the visual indicator plays properly + if (activated) { + this.activationTime = System.currentTimeMillis(); + } else { + this.activationTime = 0; + } + } + + /** + * Sets whether this button should use a pulsating effect when activated. + * + * @param pulseEffect True to enable pulsing, false for static activation indicators + */ + public void setPulseEffect(boolean pulseEffect) { + this.pulseEffect = pulseEffect; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/niravramdhanie/twod/game/entity/Door.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/Door.java new file mode 100644 index 0000000..182d550 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/Door.java @@ -0,0 +1,353 @@ +package com.niravramdhanie.twod.game.entity; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +import com.niravramdhanie.twod.game.actions.DoorAction; +import com.niravramdhanie.twod.game.utils.ResourceLoader; + +/** + * A door entity that can be opened or closed. + * When closed, it blocks player movement. + * When open, it allows the player to pass through. + * Extends Block to work with collision detection. + */ +public class Door extends Block implements DoorAction.DoorStateChangeListener { + private String id; + private boolean isOpen; + private boolean isPermanentlyOpen; + private BufferedImage doorImage; + private BufferedImage doorOpenImage; + private Color doorColor = new Color(139, 69, 19); // Brown door color + private Color frameColor = new Color(101, 67, 33); // Darker frame color + private Color openIndicatorColor = new Color(0, 255, 0, 128); // Semi-transparent green + private Color permanentIndicatorColor = new Color(255, 215, 0, 160); // Semi-transparent gold + private long openTime; // Time when the door was opened + + /** + * Creates a new door entity. + * + * @param x The x position + * @param y The y position + * @param width The width + * @param height The height + * @param id The unique ID of the door + */ + public Door(float x, float y, int width, int height, String id) { + super(x, y, width, height); + this.id = id; + this.isOpen = false; + this.isPermanentlyOpen = false; + this.openTime = 0; + + try { + // Load door images + doorImage = ResourceLoader.loadImage("/sprites/door_closed.png"); + doorOpenImage = ResourceLoader.loadImage("/sprites/door_open.png"); + } catch (Exception e) { + System.err.println("Error loading door images: " + e.getMessage()); + doorImage = null; + doorOpenImage = null; + } + } + + @Override + public void update() { + // Update door state + if (isOpen && openTime == 0) { + openTime = System.currentTimeMillis(); + } else if (!isOpen) { + openTime = 0; + } + } + + @Override + public void render(Graphics2D g) { + if (doorImage != null && doorOpenImage != null) { + // Render using images + if (isOpen) { + g.drawImage(doorOpenImage, (int)position.x, (int)position.y, width, height, null); + // Draw open indicator + if (isPermanentlyOpen) { + drawPermanentOpenIndicator(g); + } else { + drawOpenIndicator(g); + } + } else { + g.drawImage(doorImage, (int)position.x, (int)position.y, width, height, null); + } + } else { + // Fallback rendering with rectangles + if (isOpen) { + // Draw frame but no door + drawDoorFrame(g); + // Draw open indicator + if (isPermanentlyOpen) { + drawPermanentOpenIndicator(g); + } else { + drawOpenIndicator(g); + } + } else { + // Draw both frame and door + drawDoorFrame(g); + drawDoorBody(g); + } + } + } + + /** + * Draws an indicator that the door is permanently open. + * + * @param g The graphics context + */ + private void drawPermanentOpenIndicator(Graphics2D g) { + // Store original color + Color originalColor = g.getColor(); + + // Create a pulsating effect with gold color + float alpha = 0.7f; + if (openTime > 0) { + long elapsed = System.currentTimeMillis() - openTime; + alpha = 0.4f + (float)Math.abs(Math.sin(elapsed * 0.002f)) * 0.3f; + } + + // Draw a glowing golden outline around the door frame + g.setColor(new Color( + permanentIndicatorColor.getRed(), + permanentIndicatorColor.getGreen(), + permanentIndicatorColor.getBlue(), + (int)(alpha * 255) + )); + + // Draw glow over the door frame + int glowSize = 6; // Larger glow for permanent + g.fillRoundRect( + (int)position.x - glowSize, + (int)position.y - glowSize, + width + glowSize * 2, + height + glowSize * 2, + 10, 10 + ); + + // Add "PERMANENTLY OPEN" text + g.setColor(Color.WHITE); + g.setFont(new Font("Arial", Font.BOLD, 12)); + String text = "PERMANENTLY OPEN"; + FontMetrics fm = g.getFontMetrics(); + int textWidth = fm.stringWidth(text); + int textHeight = fm.getHeight(); + + // Draw background for text to improve readability + g.setColor(new Color(0, 0, 0, 180)); + g.fillRect( + (int)position.x + (width - textWidth) / 2 - 2, + (int)position.y + height + 2, + textWidth + 4, + textHeight + ); + + // Draw text + g.setColor(Color.YELLOW); + g.drawString(text, + (int)position.x + (width - textWidth) / 2, + (int)position.y + height + textHeight); + + // Restore original color + g.setColor(originalColor); + } + + /** + * Draws an indicator that the door is open. + * + * @param g The graphics context + */ + private void drawOpenIndicator(Graphics2D g) { + // Store original color + Color originalColor = g.getColor(); + + // Create a pulsating effect + float alpha = 0.6f; + if (openTime > 0) { + long elapsed = System.currentTimeMillis() - openTime; + alpha = 0.3f + (float)Math.abs(Math.sin(elapsed * 0.003f)) * 0.3f; + } + + // Draw a glowing outline around the door frame + g.setColor(new Color( + openIndicatorColor.getRed(), + openIndicatorColor.getGreen(), + openIndicatorColor.getBlue(), + (int)(alpha * 255) + )); + + // Draw glow over the door frame + int glowSize = 4; + g.fillRoundRect( + (int)position.x - glowSize, + (int)position.y - glowSize, + width + glowSize * 2, + height + glowSize * 2, + 8, 8 + ); + + // Add "OPEN" text + g.setColor(Color.WHITE); + g.setFont(new Font("Arial", Font.BOLD, 12)); + String text = "OPEN"; + FontMetrics fm = g.getFontMetrics(); + int textWidth = fm.stringWidth(text); + int textHeight = fm.getHeight(); + g.drawString(text, + (int)position.x + (width - textWidth) / 2, + (int)position.y + height + textHeight); + + // Restore original color + g.setColor(originalColor); + } + + /** + * Draws the door frame. + * + * @param g The graphics context + */ + private void drawDoorFrame(Graphics2D g) { + // Draw the door frame + g.setColor(frameColor); + + // Left side + g.fillRect((int)position.x, (int)position.y, width / 8, height); + + // Right side + g.fillRect((int)position.x + width - width / 8, (int)position.y, width / 8, height); + + // Top + g.fillRect((int)position.x, (int)position.y, width, height / 8); + + // Bottom + g.fillRect((int)position.x, (int)position.y + height - height / 8, width, height / 8); + } + + /** + * Draws the door body. + * + * @param g The graphics context + */ + private void drawDoorBody(Graphics2D g) { + // Door body fill + int doorBodyX = (int)position.x + width / 8; + int doorBodyY = (int)position.y + height / 8; + int doorBodyWidth = width - width / 4; + int doorBodyHeight = height - height / 4; + + // Create a gradient for 3D effect + GradientPaint gradient = new GradientPaint( + doorBodyX, doorBodyY, doorColor.brighter(), + doorBodyX + doorBodyWidth, doorBodyY, doorColor.darker() + ); + + // Save original paint + java.awt.Paint originalPaint = g.getPaint(); + + // Apply gradient and draw door + g.setPaint(gradient); + g.fillRect(doorBodyX, doorBodyY, doorBodyWidth, doorBodyHeight); + + // Add a door handle + g.setColor(Color.BLACK); + g.fillOval(doorBodyX + doorBodyWidth - doorBodyWidth / 6, + doorBodyY + doorBodyHeight / 2, + doorBodyWidth / 10, + doorBodyWidth / 10); + + // Restore original paint + g.setPaint(originalPaint); + } + + /** + * Opens the door. + */ + public void open() { + isOpen = true; + } + + /** + * Opens the door permanently, so it can't be closed again. + */ + public void openPermanently() { + isOpen = true; + isPermanentlyOpen = true; + System.out.println("Door " + id + " has been permanently opened!"); + } + + /** + * Checks if the door is permanently open. + * + * @return True if permanently open, false otherwise + */ + public boolean isPermanentlyOpen() { + return isPermanentlyOpen; + } + + /** + * Closes the door if it's not permanently open. + */ + public void close() { + if (!isPermanentlyOpen) { + isOpen = false; + } + } + + /** + * Checks if the door is open. + * + * @return True if open, false if closed + */ + public boolean isOpen() { + return isOpen; + } + + /** + * Gets the door ID. + * + * @return The door ID + */ + public String getId() { + return id; + } + + /** + * Sets the door ID. + * + * @param id The door ID + */ + public void setId(String id) { + this.id = id; + } + + /** + * Toggles the door state unless it's permanently open. + * + * @return The new door state + */ + public boolean toggle() { + if (!isPermanentlyOpen) { + isOpen = !isOpen; + } + return isOpen; + } + + @Override + public void onDoorStateChanged(String doorId, boolean isOpen) { + if (this.id.equals(doorId)) { + if (isOpen) { + open(); + } else if (!isPermanentlyOpen) { + close(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/entity/Entity.java b/app/src/main/java/com/niravramdhanie/twod/game/entity/Entity.java similarity index 86% rename from app/src/main/java/com/vincentramdhanie/twod/game/entity/Entity.java rename to app/src/main/java/com/niravramdhanie/twod/game/entity/Entity.java index 1593a12..5eec890 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/entity/Entity.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/entity/Entity.java @@ -1,10 +1,10 @@ -package com.vincentramdhanie.twod.game.entity; - -import com.vincentramdhanie.twod.game.utils.Vector2D; +package com.niravramdhanie.twod.game.entity; import java.awt.Graphics2D; import java.awt.Rectangle; +import com.niravramdhanie.twod.game.utils.Vector2D; + public abstract class Entity { protected Vector2D position; protected Vector2D velocity; @@ -42,6 +42,8 @@ public boolean checkCollision(Entity other) { public void setVelY(float vy) { velocity.y = vy; } public int getWidth() { return width; } public int getHeight() { return height; } + public void setWidth(int width) { this.width = width; } + public void setHeight(int height) { this.height = height; } public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } } \ No newline at end of file diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/graphics/Animation.java b/app/src/main/java/com/niravramdhanie/twod/game/graphics/Animation.java similarity index 97% rename from app/src/main/java/com/vincentramdhanie/twod/game/graphics/Animation.java rename to app/src/main/java/com/niravramdhanie/twod/game/graphics/Animation.java index 5fada75..958d641 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/graphics/Animation.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/graphics/Animation.java @@ -1,4 +1,4 @@ -package com.vincentramdhanie.twod.game.graphics; +package com.niravramdhanie.twod.game.graphics; import java.awt.image.BufferedImage; import java.util.ArrayList; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/graphics/SpriteSheet.java b/app/src/main/java/com/niravramdhanie/twod/game/graphics/SpriteSheet.java similarity index 98% rename from app/src/main/java/com/vincentramdhanie/twod/game/graphics/SpriteSheet.java rename to app/src/main/java/com/niravramdhanie/twod/game/graphics/SpriteSheet.java index be5eb13..a7f9228 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/graphics/SpriteSheet.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/graphics/SpriteSheet.java @@ -1,4 +1,4 @@ -package com.vincentramdhanie.twod.game.graphics; +package com.niravramdhanie.twod.game.graphics; import java.awt.image.BufferedImage; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/input/KeyHandler.java b/app/src/main/java/com/niravramdhanie/twod/game/input/KeyHandler.java similarity index 90% rename from app/src/main/java/com/vincentramdhanie/twod/game/input/KeyHandler.java rename to app/src/main/java/com/niravramdhanie/twod/game/input/KeyHandler.java index 4922eb0..a68bd3f 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/input/KeyHandler.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/input/KeyHandler.java @@ -1,10 +1,10 @@ -package com.vincentramdhanie.twod.game.input; - -import com.vincentramdhanie.twod.game.core.GameStateManager; +package com.niravramdhanie.twod.game.input; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import com.niravramdhanie.twod.game.core.GameStateManager; + public class KeyHandler implements KeyListener { private boolean[] keys; private GameStateManager gsm; diff --git a/app/src/main/java/com/vincentramdhanie/twod/game/input/MouseHandler.java b/app/src/main/java/com/niravramdhanie/twod/game/input/MouseHandler.java similarity index 94% rename from app/src/main/java/com/vincentramdhanie/twod/game/input/MouseHandler.java rename to app/src/main/java/com/niravramdhanie/twod/game/input/MouseHandler.java index 77545bc..c33f848 100644 --- a/app/src/main/java/com/vincentramdhanie/twod/game/input/MouseHandler.java +++ b/app/src/main/java/com/niravramdhanie/twod/game/input/MouseHandler.java @@ -1,11 +1,11 @@ -package com.vincentramdhanie.twod.game.input; - -import com.vincentramdhanie.twod.game.core.GameStateManager; +package com.niravramdhanie.twod.game.input; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import com.niravramdhanie.twod.game.core.GameStateManager; + public class MouseHandler implements MouseListener, MouseMotionListener { private int mouseX; private int mouseY; diff --git a/app/src/main/java/com/niravramdhanie/twod/game/level/Level.java b/app/src/main/java/com/niravramdhanie/twod/game/level/Level.java new file mode 100644 index 0000000..5202634 --- /dev/null +++ b/app/src/main/java/com/niravramdhanie/twod/game/level/Level.java @@ -0,0 +1,246 @@ +package com.niravramdhanie.twod.game.level; + +import java.awt.Graphics2D; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import com.niravramdhanie.twod.game.entity.Block; +import com.niravramdhanie.twod.game.entity.Button; +import com.niravramdhanie.twod.game.entity.Entity; +import com.niravramdhanie.twod.game.utils.GridSystem; + +/** + * Represents a game level with entities placed on a grid. + */ +public class Level { + private GridSystem grid; + private List entities; + private Random random; + private int screenWidth; + private int screenHeight; + + /** + * Creates a new level with the specified dimensions. + * + * @param screenWidth The width of the screen in pixels + * @param screenHeight The height of the screen in pixels + * @param gridCellSize The size of each grid cell in pixels + */ + public Level(int screenWidth, int screenHeight, int gridCellSize) { + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + this.grid = new GridSystem(gridCellSize, gridCellSize, screenWidth, screenHeight); + this.entities = new ArrayList<>(); + this.random = new Random(); + } + + /** + * Adds a block at the specified grid position. + * + * @param gridX The X position on the grid + * @param gridY The Y position on the grid + * @return True if the block was added successfully, false if the position was already occupied + */ + public boolean addBlock(int gridX, int gridY) { + int cellSize = grid.getCellSize(); + Block block = new Block(0, 0, cellSize, cellSize); + + if (grid.placeEntity(block, gridX, gridY)) { + entities.add(block); + return true; + } + + return false; + } + + /** + * Adds any entity at the specified grid position. + * + * @param entity The entity to add + * @param gridX The X position on the grid + * @param gridY The Y position on the grid + * @return True if the entity was added successfully, false if the position was already occupied + */ + public boolean addEntity(Entity entity, int gridX, int gridY) { + int cellSize = grid.getCellSize(); + entity.setWidth(cellSize); + entity.setHeight(cellSize); + + if (grid.placeEntity(entity, gridX, gridY)) { + entities.add(entity); + return true; + } + + return false; + } + + /** + * Removes an entity at the specified grid position. + * + * @param gridX The X position on the grid + * @param gridY The Y position on the grid + * @return True if an entity was removed, false otherwise + */ + public boolean removeEntityAt(int gridX, int gridY) { + Entity entity = grid.removeEntity(gridX, gridY); + if (entity != null) { + entities.remove(entity); + return true; + } + return false; + } + + /** + * Gets the entity at the specified grid position. + * + * @param gridX The X position on the grid + * @param gridY The Y position on the grid + * @return The entity at the specified position, or null if no entity is there + */ + public Entity getEntityAt(int gridX, int gridY) { + return grid.getEntity(gridX, gridY); + } + + /** + * Creates a border of blocks around the level. + */ + public void addBorderBlocks() { + int horizontalCells = grid.getHorizontalCells(); + int verticalCells = grid.getVerticalCells(); + + // Add top and bottom borders + for (int x = 0; x < horizontalCells; x++) { + addBlock(x, 0); + addBlock(x, verticalCells - 1); + } + + // Add left and right borders (skip corners as they're already added) + for (int y = 1; y < verticalCells - 1; y++) { + addBlock(0, y); + addBlock(horizontalCells - 1, y); + } + } + + /** + * Creates the first level layout with blocks and designated areas. + */ + public void createLevel1() { + clearLevel(); + + // Add border blocks + addBorderBlocks(); + } + + /** + * Creates the second level layout with blocks and designated areas. + */ + public void createLevel2() { + clearLevel(); + + // Add border blocks for level 2 + addBorderBlocks(); + + // Additional level 2 specific layout will be added here later + } + + /** + * Updates all entities in the level. + */ + public void update() { + for (Entity entity : entities) { + entity.update(); + } + } + + /** + * Renders all entities in the level. + * + * @param g The Graphics2D object to render to + */ + public void render(Graphics2D g) { + for (Entity entity : entities) { + entity.render(g); + } + } + + /** + * Gets all entities in the level. + * + * @return A list of all entities + */ + public List getEntities() { + return entities; + } + + /** + * Gets all blocks in the level. + * + * @return A list of all blocks + */ + public List getBlocks() { + List blocks = new ArrayList<>(); + for (Entity entity : entities) { + if (entity instanceof Block) { + blocks.add((Block) entity); + } + } + return blocks; + } + + /** + * Gets all buttons in the level. + * + * @return A list of all buttons + */ + public List