diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 436485c..2042922 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -369,7 +369,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Create player controller. - [x] Create camera setup. - [x] Decide first-person, third-person, or hybrid camera. Decision: hybrid camera with third person as default and optional first-person toggle. -- [ ] Implement first/third-person camera toggle. +- [x] Implement first/third-person camera toggle. - [x] Implement movement. - [ ] Implement sprinting. - [ ] Define real-world baseline walking speed. @@ -1465,4 +1465,4 @@ Earliest incomplete foundation items: Immediate next item: -- [ ] Implement first/third-person camera toggle. +- [ ] Implement sprinting. diff --git a/Content/Input/Actions/IA_ToggleCamera.uasset b/Content/Input/Actions/IA_ToggleCamera.uasset new file mode 100644 index 0000000..0f2f30d Binary files /dev/null and b/Content/Input/Actions/IA_ToggleCamera.uasset differ diff --git a/Content/Input/IMC_Default.uasset b/Content/Input/IMC_Default.uasset index a6df8b1..de77680 100644 Binary files a/Content/Input/IMC_Default.uasset and b/Content/Input/IMC_Default.uasset differ diff --git a/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset b/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset index ebc0850..bdd5c22 100644 Binary files a/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset and b/Content/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.uasset differ diff --git a/Scripts/setup_camera_toggle_input.py b/Scripts/setup_camera_toggle_input.py new file mode 100644 index 0000000..e1007b8 --- /dev/null +++ b/Scripts/setup_camera_toggle_input.py @@ -0,0 +1,88 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def create_input_action(path): + existing = unreal.EditorAssetLibrary.load_asset(path) + if existing: + return existing + + template_path = "/Game/Input/Actions/IA_Interact" + template = unreal.EditorAssetLibrary.load_asset(template_path) + if not template: + template_path = "/Game/Input/Actions/IA_Jump" + template = unreal.EditorAssetLibrary.load_asset(template_path) + if not template: + raise RuntimeError("Could not load an input action template") + + action = unreal.EditorAssetLibrary.duplicate_asset(template_path, path) + if not action: + raise RuntimeError(f"Could not create {path}") + return action + + +def set_boolean_value_type(action): + action.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN) + try: + action.set_editor_property("triggers", []) + except Exception as exc: + unreal.log_warning(f"Could not clear camera toggle triggers; keeping template defaults: {exc}") + unreal.EditorAssetLibrary.save_loaded_asset(action) + + +def mapping_exists(context, action, key_name): + mapping_data = context.get_editor_property("default_key_mappings") + for mapping in list(mapping_data.get_editor_property("mappings")): + mapping_key = mapping.get_editor_property("key") + if ( + mapping.get_editor_property("action") == action + and str(mapping_key.get_editor_property("key_name")) == key_name + ): + return True + return False + + +def map_key(context, action, key_name): + if mapping_exists(context, action, key_name): + unreal.log(f"Mapping already exists: {action.get_name()} -> {key_name}") + return + + key = unreal.Key() + key.set_editor_property("key_name", key_name) + + mapping_data = context.get_editor_property("default_key_mappings") + mappings = list(mapping_data.get_editor_property("mappings")) + new_mapping = unreal.EnhancedActionKeyMapping() + new_mapping.set_editor_property("action", action) + new_mapping.set_editor_property("key", key) + mappings.append(new_mapping) + mapping_data.set_editor_property("mappings", mappings) + context.set_editor_property("default_key_mappings", mapping_data) + + unreal.log(f"Added mapping: {action.get_name()} -> {key_name}") + + +def main(): + toggle_action = create_input_action("/Game/Input/Actions/IA_ToggleCamera") + set_boolean_value_type(toggle_action) + + context = load("/Game/Input/IMC_Default") + map_key(context, toggle_action, "V") + map_key(context, toggle_action, "Gamepad_RightThumbstick") + unreal.EditorAssetLibrary.save_loaded_asset(context) + + character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_cdo = unreal.get_default_object(character_bp.generated_class()) + character_cdo.set_editor_property("ToggleCameraAction", toggle_action) + unreal.EditorAssetLibrary.save_loaded_asset(character_bp) + + unreal.log("Agrarian camera toggle input setup complete.") + + +main() diff --git a/Scripts/verify_camera_toggle_input.py b/Scripts/verify_camera_toggle_input.py new file mode 100644 index 0000000..aa6e4b4 --- /dev/null +++ b/Scripts/verify_camera_toggle_input.py @@ -0,0 +1,44 @@ +import unreal + + +def load(path): + asset = unreal.EditorAssetLibrary.load_asset(path) + if not asset: + raise RuntimeError(f"Could not load {path}") + return asset + + +def mapping_found(context, action, key_name): + mapping_data = context.get_editor_property("default_key_mappings") + for mapping in list(mapping_data.get_editor_property("mappings")): + mapping_key = mapping.get_editor_property("key") + if ( + mapping.get_editor_property("action") == action + and str(mapping_key.get_editor_property("key_name")) == key_name + ): + return True + return False + + +def main(): + action = load("/Game/Input/Actions/IA_ToggleCamera") + context = load("/Game/Input/IMC_Default") + character_bp = load("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter") + character_cdo = unreal.get_default_object(character_bp.generated_class()) + + missing = [] + for key_name in ["V", "Gamepad_RightThumbstick"]: + if not mapping_found(context, action, key_name): + missing.append(f"missing mapping {key_name}") + + assigned_action = character_cdo.get_editor_property("ToggleCameraAction") + if assigned_action != action: + missing.append("BP_ThirdPersonCharacter ToggleCameraAction is not IA_ToggleCamera") + + if missing: + raise RuntimeError("Camera toggle input verification failed: " + "; ".join(missing)) + + unreal.log("Agrarian camera toggle input verification complete.") + + +main() diff --git a/Source/AgrarianGame/AgrarianGameCharacter.cpp b/Source/AgrarianGame/AgrarianGameCharacter.cpp index 835b6cc..1d423a8 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.cpp +++ b/Source/AgrarianGame/AgrarianGameCharacter.cpp @@ -43,7 +43,7 @@ AAgrarianGameCharacter::AAgrarianGameCharacter() // Create a camera boom (pulls in towards the player if there is a collision) CameraBoom = CreateDefaultSubobject(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); - CameraBoom->TargetArmLength = 400.0f; + CameraBoom->TargetArmLength = ThirdPersonCameraDistance; CameraBoom->bUsePawnControlRotation = true; // Create a follow camera @@ -80,6 +80,11 @@ void AAgrarianGameCharacter::SetupPlayerInputComponent(UInputComponent* PlayerIn { EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::Interact); } + + if (ToggleCameraAction) + { + EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Started, this, &AAgrarianGameCharacter::ToggleCameraPerspective); + } } else { @@ -110,6 +115,46 @@ void AAgrarianGameCharacter::Interact() TryInteract(); } +void AAgrarianGameCharacter::ToggleCameraPerspective() +{ + SetFirstPersonCamera(!bFirstPersonCamera); +} + +void AAgrarianGameCharacter::SetFirstPersonCamera(bool bEnableFirstPerson) +{ + bFirstPersonCamera = bEnableFirstPerson; + + if (!CameraBoom || !FollowCamera) + { + return; + } + + if (bFirstPersonCamera) + { + CameraBoom->TargetArmLength = 0.0f; + CameraBoom->SocketOffset = FirstPersonCameraOffset; + CameraBoom->bDoCollisionTest = false; + FollowCamera->bUsePawnControlRotation = false; + + if (GetMesh()) + { + GetMesh()->SetOwnerNoSee(true); + } + } + else + { + CameraBoom->TargetArmLength = ThirdPersonCameraDistance; + CameraBoom->SocketOffset = FVector::ZeroVector; + CameraBoom->bDoCollisionTest = true; + FollowCamera->bUsePawnControlRotation = false; + + if (GetMesh()) + { + GetMesh()->SetOwnerNoSee(false); + } + } +} + void AAgrarianGameCharacter::DoMove(float Right, float Forward) { if (GetController() != nullptr) diff --git a/Source/AgrarianGame/AgrarianGameCharacter.h b/Source/AgrarianGame/AgrarianGameCharacter.h index 84105c5..346d3cb 100644 --- a/Source/AgrarianGame/AgrarianGameCharacter.h +++ b/Source/AgrarianGame/AgrarianGameCharacter.h @@ -73,10 +73,26 @@ protected: UPROPERTY(EditAnywhere, Category="Input") UInputAction* InteractAction; + /** Toggle between third-person and first-person camera views. */ + UPROPERTY(EditAnywhere, Category="Input") + UInputAction* ToggleCameraAction; + /** How far the player can interact with MVP objects. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Interaction", meta = (ClampMin = "100")) float InteractionDistance = 450.0f; + /** Third-person spring arm distance used when returning from first person. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Camera", meta = (ClampMin = "0")) + float ThirdPersonCameraDistance = 400.0f; + + /** Local first-person camera offset relative to the capsule root. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Agrarian|Camera") + FVector FirstPersonCameraOffset = FVector(0.0f, 0.0f, 72.0f); + + /** True when this local character is using the optional first-person view. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Agrarian|Camera", meta = (AllowPrivateAccess = "true")) + bool bFirstPersonCamera = false; + public: /** Constructor */ @@ -98,6 +114,12 @@ protected: /** Called for interaction input */ void Interact(); + /** Called for camera perspective toggle input */ + void ToggleCameraPerspective(); + + /** Applies local camera presentation state. */ + void SetFirstPersonCamera(bool bEnableFirstPerson); + public: /** Handles move inputs from either controls or UI interfaces */ @@ -120,6 +142,10 @@ public: UFUNCTION(BlueprintCallable, Category="Agrarian|Interaction") virtual void TryInteract(); + /** Returns true when this local character is using first-person camera presentation. */ + UFUNCTION(BlueprintPure, Category="Agrarian|Camera") + bool IsFirstPersonCamera() const { return bFirstPersonCamera; } + /** Server-authoritative interaction entry point. */ UFUNCTION(Server, Reliable) void ServerInteract(AActor* TargetActor); @@ -144,4 +170,3 @@ public: /** Returns BuildingPlacementComponent subobject **/ FORCEINLINE UAgrarianBuildingPlacementComponent* GetBuildingPlacementComponent() const { return BuildingPlacementComponent; } }; -