(Feat) Add Harvetable
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,6 +32,13 @@
|
|||||||
"Editor"
|
"Editor"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "PythonScriptPlugin",
|
||||||
|
"Enabled": true,
|
||||||
|
"TargetAllowList": [
|
||||||
|
"Editor"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "OnlineSubsystemSteam",
|
"Name": "OnlineSubsystemSteam",
|
||||||
"Enabled": true
|
"Enabled": true
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Rangement unique de Content/Game.
|
||||||
|
|
||||||
|
A lancer depuis l'editeur : Tools > Execute Python Script.
|
||||||
|
Mettre DRY_RUN a True pour une simulation qui ne touche a rien, puis a False.
|
||||||
|
|
||||||
|
Pourquoi un script et pas du drag & drop : ~70 operations, dont des renommages
|
||||||
|
qui doivent se faire dans un ordre precis (un materiau prend le nom qu'une
|
||||||
|
instance vient de liberer). Une erreur de destination se voit ici, pas trois
|
||||||
|
jours plus tard sur un asset au mauvais endroit.
|
||||||
|
|
||||||
|
Unreal se charge lui-meme de reecrire les references : rename_asset laisse un
|
||||||
|
redirector derriere lui, et fixup_referencers() les resout a la fin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unreal
|
||||||
|
|
||||||
|
DRY_RUN = True
|
||||||
|
|
||||||
|
EAL = unreal.EditorAssetLibrary
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Assets a supprimer -- verifie sans aucun referenceur dans tout le projet.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DELETIONS = [
|
||||||
|
# Doublon reel : le M_Pickaxe utilise par le mesh est celui de 3d/, pas celui-ci.
|
||||||
|
"/Game/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe",
|
||||||
|
"/Game/Game/Audio/Chest/Chest_Closes_2",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Renommages d'assets. L'ORDRE COMPTE dans le bloc Table : l'instance libere
|
||||||
|
# le nom M_Table avant que le materiau parent ne le prenne.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ASSET_MOVES = [
|
||||||
|
# --- Art / Building / Table -------------------------------------------
|
||||||
|
("/Game/Game/3d/Building/Table/M_Table",
|
||||||
|
"/Game/Game/Art/Building/Table/MI_Table"),
|
||||||
|
("/Game/Game/3d/Building/Table/textures/M_table_albedo_Mat",
|
||||||
|
"/Game/Game/Art/Building/Table/M_Table"),
|
||||||
|
("/Game/Game/3d/Building/Table/Table_FBX",
|
||||||
|
"/Game/Game/Art/Building/Table/SM_Table"),
|
||||||
|
("/Game/Game/3d/Building/Table/textures/M_table_albedo",
|
||||||
|
"/Game/Game/Art/Building/Table/T_Table_BaseColor"),
|
||||||
|
("/Game/Game/3d/Building/Table/textures/M_table_Metal",
|
||||||
|
"/Game/Game/Art/Building/Table/T_Table_Metallic"),
|
||||||
|
("/Game/Game/3d/Building/Table/textures/M_table_Normal",
|
||||||
|
"/Game/Game/Art/Building/Table/T_Table_Normal"),
|
||||||
|
("/Game/Game/3d/Building/Table/textures/M_table_Roughness",
|
||||||
|
"/Game/Game/Art/Building/Table/T_Table_Roughness"),
|
||||||
|
|
||||||
|
# --- Art / Cooking -----------------------------------------------------
|
||||||
|
# Les suffixes numeriques sont conserves : rien ne dit lequel est lequel,
|
||||||
|
# et inventer un nom parlant ici serait une supposition.
|
||||||
|
("/Game/Game/3d/Cooking/Campfire_Logs",
|
||||||
|
"/Game/Game/Art/Cooking/SM_Campfire_Logs"),
|
||||||
|
("/Game/Game/3d/Cooking/Campfire_Logs_001",
|
||||||
|
"/Game/Game/Art/Cooking/SM_Campfire_Logs_001"),
|
||||||
|
("/Game/Game/3d/Cooking/Campfire_Logs_002",
|
||||||
|
"/Game/Game/Art/Cooking/SM_Campfire_Logs_002"),
|
||||||
|
("/Game/Game/3d/Cooking/Campfire_Logs_003",
|
||||||
|
"/Game/Game/Art/Cooking/SM_Campfire_Logs_003"),
|
||||||
|
("/Game/Game/3d/Cooking/LogS",
|
||||||
|
"/Game/Game/Art/Cooking/SM_LogS"),
|
||||||
|
("/Game/Game/3d/Cooking/Logs1",
|
||||||
|
"/Game/Game/Art/Cooking/SM_Logs1"),
|
||||||
|
("/Game/Game/3d/Cooking/Rocks_003",
|
||||||
|
"/Game/Game/Art/Cooking/MI_Rocks_003"),
|
||||||
|
("/Game/Game/3d/Cooking/Wood_004",
|
||||||
|
"/Game/Game/Art/Cooking/MI_Wood_004"),
|
||||||
|
("/Game/Game/3d/Cooking/Wood_light_001",
|
||||||
|
"/Game/Game/Art/Cooking/MI_Wood_light_001"),
|
||||||
|
|
||||||
|
# --- Art / Tools / Axe -------------------------------------------------
|
||||||
|
# Le materiau s'appelait "Wood_Axe", exactement comme le mesh : deux assets
|
||||||
|
# homonymes a deux endroits, c'est ce qui rendait le dossier illisible.
|
||||||
|
("/Game/Game/3d/Tools/Wood_Axe",
|
||||||
|
"/Game/Game/Art/Tools/Axe/SM_Wood_Axe"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe",
|
||||||
|
"/Game/Game/Art/Tools/Axe/M_Wood_Axe"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor",
|
||||||
|
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_BaseColor"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal",
|
||||||
|
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_Normal"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic",
|
||||||
|
"/Game/Game/Art/Tools/Axe/T_Wood_Axe_ORM"),
|
||||||
|
# Orphelin, conserve : la hache en bois est un chantier ouvert.
|
||||||
|
("/Game/Game/Material/Tools/Woods/Axe/M_Axe",
|
||||||
|
"/Game/Game/Art/Tools/Axe/M_Axe"),
|
||||||
|
|
||||||
|
# --- Art / Tools / Pickaxe ---------------------------------------------
|
||||||
|
# Les textures portaient le nom de la hache alors qu'elles alimentent la
|
||||||
|
# pioche : on remet le nom en accord avec l'usage reel.
|
||||||
|
("/Game/Game/3d/Tools/Wood_Pickaxe",
|
||||||
|
"/Game/Game/Art/Tools/Pickaxe/SM_Wood_Pickaxe"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe",
|
||||||
|
"/Game/Game/Art/Tools/Pickaxe/M_Wood_Pickaxe"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB",
|
||||||
|
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_BaseColor"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw",
|
||||||
|
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_Normal"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw",
|
||||||
|
"/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_ORM"),
|
||||||
|
|
||||||
|
# --- Art / Tools / Sword -----------------------------------------------
|
||||||
|
("/Game/Game/3d/Tools/Wood_Sword",
|
||||||
|
"/Game/Game/Art/Tools/Sword/SM_Wood_Sword"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor",
|
||||||
|
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_BaseColor"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal",
|
||||||
|
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_Normal"),
|
||||||
|
("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic",
|
||||||
|
"/Game/Game/Art/Tools/Sword/T_Wood_Sword_ORM"),
|
||||||
|
|
||||||
|
# --- Art / Character / Arms --------------------------------------------
|
||||||
|
("/Game/Game/Material/Fps_Arm/M_Fps_Arm",
|
||||||
|
"/Game/Game/Art/Character/Arms/M_Fps_Arm"),
|
||||||
|
("/Game/Game/Material/Fps_Arm/MaleBody_M_Base_color_sRGB",
|
||||||
|
"/Game/Game/Art/Character/Arms/T_Fps_Arm_BaseColor"),
|
||||||
|
|
||||||
|
# --- Blueprints --------------------------------------------------------
|
||||||
|
# Framework = ce qui n'est pas un acteur du monde : GameMode, GameInstance,
|
||||||
|
# PlayerControllers. Le critere est celui de CLAUDE.md, pas le type d'asset.
|
||||||
|
("/Game/Game/Player/BP_FpsPlayer",
|
||||||
|
"/Game/Game/Blueprints/Player/BP_FpsPlayer"),
|
||||||
|
("/Game/Game/Player/BP_FpsPlayerController",
|
||||||
|
"/Game/Game/Blueprints/Framework/BP_FpsPlayerController"),
|
||||||
|
("/Game/Game/Menu/BP_MainMenuPlayerController",
|
||||||
|
"/Game/Game/Blueprints/Framework/BP_MainMenuPlayerController"),
|
||||||
|
("/Game/Game/GameMode/GM_FpsPlayer",
|
||||||
|
"/Game/Game/Blueprints/Framework/GM_FpsPlayer"),
|
||||||
|
("/Game/Game/GameMode/GM_MainMenu",
|
||||||
|
"/Game/Game/Blueprints/Framework/GM_MainMenu"),
|
||||||
|
("/Game/Game/Game-Instance/BP_SurvivalGameInstance",
|
||||||
|
"/Game/Game/Blueprints/Framework/BP_SurvivalGameInstance"),
|
||||||
|
("/Game/Game/PickupItem/BP_Pickup_Test",
|
||||||
|
"/Game/Game/Blueprints/World/BP_Pickup_Test"),
|
||||||
|
("/Game/Game/Building/Cooking/BP_Campfire",
|
||||||
|
"/Game/Game/Blueprints/World/BP_Campfire"),
|
||||||
|
("/Game/Game/Building/Crafting/BP_Workbench",
|
||||||
|
"/Game/Game/Blueprints/World/BP_Workbench"),
|
||||||
|
("/Game/Game/Building/Storage/BP_Chest_Wooden",
|
||||||
|
"/Game/Game/Blueprints/World/BP_Chest_Wooden"),
|
||||||
|
("/Game/Game/BP/Menu/CharaterCustomisation/BP_CharacterPreview",
|
||||||
|
"/Game/Game/Blueprints/Menu/BP_CharacterPreview"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Renommages qui doivent avoir lieu APRES un deplacement de dossier, parce que
|
||||||
|
# leur chemin source n'existe qu'une fois le dossier deplace.
|
||||||
|
ASSET_MOVES_LATE = [
|
||||||
|
# Le squelette n'est pas une animation : il sort de Sequences/.
|
||||||
|
("/Game/Game/Animation/FirstPerson/Sequences/BasePos_Skeleton",
|
||||||
|
"/Game/Game/Animation/FirstPerson/BasePos_Skeleton"),
|
||||||
|
# Nom colle deux fois par un import rate.
|
||||||
|
("/Game/Game/UI/Widgets/Player/WBP_PlayerNameplateWBP_PlayerNameplate",
|
||||||
|
"/Game/Game/UI/Widgets/Player/WBP_PlayerNameplate"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Deplacements de dossiers entiers.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DIR_MOVES = [
|
||||||
|
# --- Animation ---------------------------------------------------------
|
||||||
|
("/Game/Game/Animation/Fps",
|
||||||
|
"/Game/Game/Animation/FirstPerson/Sequences"),
|
||||||
|
("/Game/Game/Animation/ThirdPerson/RetargetedAnimations",
|
||||||
|
"/Game/Game/Animation/ThirdPerson/Sequences"),
|
||||||
|
|
||||||
|
# --- Audio : fusion de Sounds/ dans Audio/ -----------------------------
|
||||||
|
("/Game/Game/Sounds/Mix", "/Game/Game/Audio/Mix"),
|
||||||
|
("/Game/Game/Sounds/Music", "/Game/Game/Audio/Music"),
|
||||||
|
("/Game/Game/Sounds/Playlists", "/Game/Game/Audio/Playlists"),
|
||||||
|
("/Game/Game/Sounds/UI", "/Game/Game/Audio/UI"),
|
||||||
|
# FootSteps -> Footsteps : Windows ne distingue pas la casse, donc on passe
|
||||||
|
# par un nom intermediaire, sinon le renommage est un no-op silencieux.
|
||||||
|
("/Game/Game/Audio/FootSteps", "/Game/Game/Audio/_TmpFootsteps"),
|
||||||
|
("/Game/Game/Audio/_TmpFootsteps", "/Game/Game/Audio/Footsteps"),
|
||||||
|
|
||||||
|
# --- Data --------------------------------------------------------------
|
||||||
|
("/Game/Game/Data/Harvetable", "/Game/Game/Data/Harvestable"),
|
||||||
|
("/Game/Game/Data/Item", "/Game/Game/Data/Items"),
|
||||||
|
|
||||||
|
# --- UI : les widgets d'un cote, les ressources de l'autre -------------
|
||||||
|
("/Game/Game/UI/Images/512x512", "/Game/Game/UI/Icons/Items"),
|
||||||
|
("/Game/Game/UI/Images/Tools", "/Game/Game/UI/Icons/Tools"),
|
||||||
|
("/Game/Game/UI/Font", "/Game/Game/UI/Fonts"),
|
||||||
|
("/Game/Game/UI/Confirm", "/Game/Game/UI/Widgets/Confirm"),
|
||||||
|
("/Game/Game/UI/Craft", "/Game/Game/UI/Widgets/Craft"),
|
||||||
|
("/Game/Game/UI/CrossHair", "/Game/Game/UI/Widgets/Crosshair"),
|
||||||
|
("/Game/Game/UI/Customization", "/Game/Game/UI/Widgets/Customization"),
|
||||||
|
("/Game/Game/UI/Fade", "/Game/Game/UI/Widgets/Fade"),
|
||||||
|
("/Game/Game/UI/Interaction", "/Game/Game/UI/Widgets/Interaction"),
|
||||||
|
("/Game/Game/UI/Inventory", "/Game/Game/UI/Widgets/Inventory"),
|
||||||
|
("/Game/Game/UI/Menu", "/Game/Game/UI/Widgets/MainMenu"),
|
||||||
|
("/Game/Game/UI/Pause", "/Game/Game/UI/Widgets/Pause"),
|
||||||
|
("/Game/Game/UI/Player", "/Game/Game/UI/Widgets/Player"),
|
||||||
|
("/Game/Game/UI/Setting", "/Game/Game/UI/Widgets/Settings"),
|
||||||
|
("/Game/Game/UI/Stats", "/Game/Game/UI/Widgets/Stats"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Les deux icones egarees hors de Content/Game.
|
||||||
|
STRAY_MOVES = [
|
||||||
|
("/Game/UI/Icons/T_Icon_Food_06", "/Game/Game/UI/Icons/Items/T_Icon_Food_06"),
|
||||||
|
("/Game/UI/Icons/T_Icon_Food_07", "/Game/Game/UI/Icons/Items/T_Icon_Food_07"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Dossiers vides une fois tout deplace ET les redirectors resolus.
|
||||||
|
EMPTY_DIRS = [
|
||||||
|
"/Game/Game/3d",
|
||||||
|
"/Game/Game/Material",
|
||||||
|
"/Game/Game/Player",
|
||||||
|
"/Game/Game/Menu",
|
||||||
|
"/Game/Game/GameMode",
|
||||||
|
"/Game/Game/Game-Instance",
|
||||||
|
"/Game/Game/PickupItem",
|
||||||
|
"/Game/Game/Building",
|
||||||
|
"/Game/Game/BP",
|
||||||
|
"/Game/Game/Sounds",
|
||||||
|
"/Game/Game/Animation/Anim_BP",
|
||||||
|
"/Game/Game/UI/Images",
|
||||||
|
"/Game/UI",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
stats = {"ok": 0, "skip": 0, "fail": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def log(kind, message):
|
||||||
|
stats[kind] = stats.get(kind, 0) + 1
|
||||||
|
prefix = {"ok": " OK ", "skip": " SKIP ", "fail": " FAIL "}[kind]
|
||||||
|
unreal.log("{}{}".format(prefix, message))
|
||||||
|
|
||||||
|
|
||||||
|
def move_asset(src, dst):
|
||||||
|
if not EAL.does_asset_exist(src):
|
||||||
|
log("skip", "source absente : {}".format(src))
|
||||||
|
return
|
||||||
|
if EAL.does_asset_exist(dst):
|
||||||
|
log("fail", "destination DEJA OCCUPEE : {}".format(dst))
|
||||||
|
return
|
||||||
|
if DRY_RUN:
|
||||||
|
log("ok", "{} -> {}".format(src, dst))
|
||||||
|
return
|
||||||
|
if EAL.rename_asset(src, dst):
|
||||||
|
log("ok", "{} -> {}".format(src, dst))
|
||||||
|
else:
|
||||||
|
log("fail", "rename_asset a echoue : {}".format(src))
|
||||||
|
|
||||||
|
|
||||||
|
def move_dir(src, dst):
|
||||||
|
if not EAL.does_directory_exist(src):
|
||||||
|
log("skip", "dossier absent : {}".format(src))
|
||||||
|
return
|
||||||
|
if DRY_RUN:
|
||||||
|
log("ok", "[dir] {} -> {}".format(src, dst))
|
||||||
|
return
|
||||||
|
if EAL.rename_directory(src, dst):
|
||||||
|
log("ok", "[dir] {} -> {}".format(src, dst))
|
||||||
|
else:
|
||||||
|
log("fail", "rename_directory a echoue : {}".format(src))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_asset(path):
|
||||||
|
if not EAL.does_asset_exist(path):
|
||||||
|
log("skip", "deja absent : {}".format(path))
|
||||||
|
return
|
||||||
|
if DRY_RUN:
|
||||||
|
log("ok", "[del] {}".format(path))
|
||||||
|
return
|
||||||
|
if EAL.delete_asset(path):
|
||||||
|
log("ok", "[del] {}".format(path))
|
||||||
|
else:
|
||||||
|
log("fail", "delete_asset a echoue : {}".format(path))
|
||||||
|
|
||||||
|
|
||||||
|
def fixup_redirectors():
|
||||||
|
"""Resout tous les ObjectRedirector du projet et les supprime.
|
||||||
|
|
||||||
|
C'est l'etape que personne n'avait lancee : deux redirectors de 2024
|
||||||
|
trainaient encore dans Data/Item.
|
||||||
|
"""
|
||||||
|
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||||
|
registry.search_all_assets(True)
|
||||||
|
|
||||||
|
asset_filter = unreal.ARFilter(
|
||||||
|
class_paths=[unreal.TopLevelAssetPath("/Script/CoreUObject", "ObjectRedirector")],
|
||||||
|
package_paths=["/Game"],
|
||||||
|
recursive_paths=True,
|
||||||
|
)
|
||||||
|
found = registry.get_assets(asset_filter)
|
||||||
|
unreal.log("Redirectors trouves : {}".format(len(found)))
|
||||||
|
if not found:
|
||||||
|
return
|
||||||
|
|
||||||
|
for data in found:
|
||||||
|
unreal.log(" {}".format(data.package_name))
|
||||||
|
|
||||||
|
if DRY_RUN:
|
||||||
|
return
|
||||||
|
|
||||||
|
redirectors = [data.get_asset() for data in found]
|
||||||
|
unreal.AssetToolsHelpers.get_asset_tools().fixup_referencers(redirectors)
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
mode = "SIMULATION (rien n'est modifie)" if DRY_RUN else "EXECUTION REELLE"
|
||||||
|
unreal.log("=" * 70)
|
||||||
|
unreal.log("Rangement de Content/Game -- {}".format(mode))
|
||||||
|
unreal.log("=" * 70)
|
||||||
|
|
||||||
|
unreal.log("\n--- 1. Redirectors existants ---")
|
||||||
|
fixup_redirectors()
|
||||||
|
|
||||||
|
unreal.log("\n--- 2. Suppressions ---")
|
||||||
|
for path in DELETIONS:
|
||||||
|
delete_asset(path)
|
||||||
|
|
||||||
|
unreal.log("\n--- 3. Assets ---")
|
||||||
|
for src, dst in ASSET_MOVES:
|
||||||
|
move_asset(src, dst)
|
||||||
|
|
||||||
|
unreal.log("\n--- 4. Dossiers ---")
|
||||||
|
for src, dst in DIR_MOVES:
|
||||||
|
move_dir(src, dst)
|
||||||
|
|
||||||
|
unreal.log("\n--- 5. Assets dependant d'un dossier deplace ---")
|
||||||
|
for src, dst in ASSET_MOVES_LATE:
|
||||||
|
move_asset(src, dst)
|
||||||
|
|
||||||
|
unreal.log("\n--- 6. Icones egarees hors de Content/Game ---")
|
||||||
|
for src, dst in STRAY_MOVES:
|
||||||
|
move_asset(src, dst)
|
||||||
|
|
||||||
|
# Obligatoire avant de supprimer les dossiers d'origine : chaque
|
||||||
|
# deplacement y a laisse un redirector, donc ils ne sont pas vides.
|
||||||
|
unreal.log("\n--- 7. Resolution des redirectors crees ---")
|
||||||
|
fixup_redirectors()
|
||||||
|
|
||||||
|
unreal.log("\n--- 8. Dossiers vides ---")
|
||||||
|
for path in EMPTY_DIRS:
|
||||||
|
if not EAL.does_directory_exist(path):
|
||||||
|
log("skip", "deja absent : {}".format(path))
|
||||||
|
continue
|
||||||
|
if DRY_RUN:
|
||||||
|
log("ok", "[rmdir] {}".format(path))
|
||||||
|
continue
|
||||||
|
if EAL.delete_directory(path):
|
||||||
|
log("ok", "[rmdir] {}".format(path))
|
||||||
|
else:
|
||||||
|
log("fail", "dossier non vide : {}".format(path))
|
||||||
|
|
||||||
|
if not DRY_RUN:
|
||||||
|
unreal.log("\n--- 9. Sauvegarde ---")
|
||||||
|
EAL.save_directory("/Game/Game", only_if_is_dirty=False, recursive=True)
|
||||||
|
|
||||||
|
unreal.log("\n" + "=" * 70)
|
||||||
|
unreal.log("Termine -- OK: {} SKIP: {} FAIL: {}".format(
|
||||||
|
stats["ok"], stats["skip"], stats["fail"]))
|
||||||
|
if stats["fail"]:
|
||||||
|
unreal.log_error("{} operation(s) en echec : lire le log ci-dessus.".format(stats["fail"]))
|
||||||
|
unreal.log("=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
run()
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "CraftingComponent.h"
|
#include "CraftingComponent.h"
|
||||||
#include "FootstepComponent.h"
|
#include "FootstepComponent.h"
|
||||||
#include "FpsPlayerController.h"
|
#include "FpsPlayerController.h"
|
||||||
|
#include "HarvestingComponent.h"
|
||||||
#include "HeadBobComponent.h"
|
#include "HeadBobComponent.h"
|
||||||
#include "HeldItemComponent.h"
|
#include "HeldItemComponent.h"
|
||||||
#include "InteractionComponent.h"
|
#include "InteractionComponent.h"
|
||||||
@@ -115,6 +116,7 @@ AFpsPlayer::AFpsPlayer()
|
|||||||
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
|
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
|
||||||
HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
|
HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
|
||||||
FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));
|
FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));
|
||||||
|
HarvestingComponent = CreateDefaultSubobject<UHarvestingComponent>(TEXT("HarvestingComponent"));
|
||||||
|
|
||||||
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
|
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
|
||||||
// mais on garde son ombre.
|
// mais on garde son ombre.
|
||||||
@@ -786,45 +788,60 @@ void AFpsPlayer::TriggerHeldItemAction()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le trace part d'ICI, chez celui qui frappe, et sans condition sur ce qu'il
|
||||||
|
// tient. Deux raisons de ne pas le reserver aux outils : une ressource dont
|
||||||
|
// RequiredTool vaut None se cueille a mains nues, et lire l'inventaire local
|
||||||
|
// pour trier ferait dependre le trace d'un etat qui peut avoir une frame de
|
||||||
|
// retard. Le cout est un unique balayage spherique par CLIC -- pas par frame.
|
||||||
|
FVector ImpactPoint;
|
||||||
|
FVector HitDirection;
|
||||||
|
AActor* SwingTarget = HarvestingComponent
|
||||||
|
? HarvestingComponent->TraceSwingTarget(ImpactPoint, HitDirection)
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
// L'hote agit tout de suite, le client demande. On envoie meme si le slot
|
// L'hote agit tout de suite, le client demande. On envoie meme si le slot
|
||||||
// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
|
// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
|
||||||
// peut avoir une frame d'avance sur le notre.
|
// peut avoir une frame d'avance sur le notre.
|
||||||
if (!HasAuthority())
|
if (!HasAuthority())
|
||||||
{
|
{
|
||||||
Server_UseItem(PendingUseSlotIndex);
|
Server_UseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformUseItem(PendingUseSlotIndex);
|
PerformUseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
|
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex, AActor* SwingTarget,
|
||||||
|
FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection)
|
||||||
{
|
{
|
||||||
PerformUseItem(SlotIndex);
|
PerformUseItem(SlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AFpsPlayer::PerformUseItem(int32 SlotIndex)
|
void AFpsPlayer::PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint,
|
||||||
|
const FVector& HitDirection)
|
||||||
{
|
{
|
||||||
if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex))
|
if (!HasAuthority())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex];
|
// Un slot invalide ou vide n'est PAS une sortie : c'est le cas des mains nues,
|
||||||
if (Selected.IsEmpty())
|
// qui frappent quand meme -- un buisson de baies se cueille au poing. La
|
||||||
|
// version precedente sortait ici, quand seuls les consommables agissaient.
|
||||||
|
const FInventorySlot* Selected = InventoryComponent->GetSlots().IsValidIndex(SlotIndex)
|
||||||
|
? &InventoryComponent->GetSlots()[SlotIndex]
|
||||||
|
: nullptr;
|
||||||
|
|
||||||
|
const UItemDataAsset* Item = (Selected && !Selected->IsEmpty()) ? Selected->Item.Get() : nullptr;
|
||||||
|
|
||||||
|
if (!Item)
|
||||||
{
|
{
|
||||||
|
PerformToolSwing(nullptr, INDEX_NONE, SwingTarget, ImpactPoint, HitDirection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UItemDataAsset* Item = Selected.Item;
|
if (Item->bIsConsumable)
|
||||||
|
|
||||||
// Objet non consommable : on ne fait rien pour l'instant. C'est ici que
|
|
||||||
// viendront les outils (frapper), les armes (tirer), les placeables.
|
|
||||||
if (!Item->bIsConsumable)
|
|
||||||
{
|
{
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
|
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
|
||||||
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
|
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
|
||||||
|
|
||||||
@@ -833,6 +850,52 @@ void AFpsPlayer::PerformUseItem(int32 SlotIndex)
|
|||||||
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
|
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
|
||||||
// n'importe quelle pile du meme objet, y compris dans le sac.
|
// n'importe quelle pile du meme objet, y compris dans le sac.
|
||||||
InventoryComponent->ConsumeUse(SlotIndex);
|
InventoryComponent->ConsumeUse(SlotIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tout le reste frappe : les outils avec leur puissance, les autres objets
|
||||||
|
// comme un poing. Un objet qui n'est ni consommable ni outil ne peut donc rien
|
||||||
|
// contre un arbre -- AHarvestableResource::AcceptsTool l'y refuse -- mais
|
||||||
|
// cueille les ressources a mains nues, ce qui est exactement ce qu'on veut :
|
||||||
|
// tenir une pomme n'empeche pas de ramasser des baies.
|
||||||
|
PerformToolSwing(Item, SlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AFpsPlayer::PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||||
|
{
|
||||||
|
if (!HarvestingComponent || !SwingTarget)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EHarvestResult Result = HarvestingComponent->ApplySwing(SwingTarget, Tool, ImpactPoint, HitDirection);
|
||||||
|
|
||||||
|
// SEULS les deux resultats qui portent usent l'outil. Un mauvais outil, une
|
||||||
|
// souche ou un coup dans le vide n'en consomment aucune charge : abimer une
|
||||||
|
// hache en tapant un rocher serait une punition que rien a l'ecran n'explique.
|
||||||
|
if (Result != EHarvestResult::Harvested && Result != EHarvestResult::Depleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Tool || !Tool->IsTool() || SlotIndex == INDEX_NONE)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un outil laisse a MaxUses = 1 disparaitrait au PREMIER coup : ConsumeUse
|
||||||
|
// retire alors l'exemplaire entier. On refuse plutot que de faire s'evaporer
|
||||||
|
// la hache que le joueur vient de fabriquer, et on le dit -- le symptome, lui,
|
||||||
|
// se lit comme « ma hache a disparu » et se cherche tres longtemps.
|
||||||
|
if (Tool->MaxUses <= 1)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : l'outil %s a MaxUses = 1, il casserait au premier coup. Usure ignoree."),
|
||||||
|
*GetNameSafe(Tool));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryComponent->ConsumeUse(SlotIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
FVector AFpsPlayer::PlaceOnGround(const FVector& Start) const
|
FVector AFpsPlayer::PlaceOnGround(const FVector& Start) const
|
||||||
@@ -940,6 +1003,11 @@ bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotInd
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||||
|
{
|
||||||
|
DropAtFeet(Item, Quantity, RemainingUses);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AFpsPlayer::DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||||
{
|
{
|
||||||
if (!Item || Quantity <= 0)
|
if (!Item || Quantity <= 0)
|
||||||
{
|
{
|
||||||
@@ -948,7 +1016,7 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
|
|||||||
|
|
||||||
if (!DroppedPickupClass)
|
if (!DroppedPickupClass)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
|
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sont perdus."),
|
||||||
Quantity, *GetNameSafe(Item));
|
Quantity, *GetNameSafe(Item));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -960,16 +1028,43 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
|
|||||||
|
|
||||||
// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
|
// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
|
||||||
// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
|
// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
|
||||||
// une cloison qu'on ne regarde meme pas.
|
// une cloison qu'on ne regarde meme pas. Le meme raisonnement vaut pour le
|
||||||
|
// surplus de recolte -- le joueur regarde l'arbre, pas le sol.
|
||||||
const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;
|
const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;
|
||||||
|
|
||||||
if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
|
if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."),
|
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s."),
|
||||||
Quantity, *GetNameSafe(Item));
|
Quantity, *GetNameSafe(Item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 AFpsPlayer::GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||||
|
{
|
||||||
|
if (!Item || Quantity <= 0 || !InventoryComponent)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garde-fou et pas simple precaution : engendrer un pickup chez un client
|
||||||
|
// donnerait un objet visible de lui seul, efface au premier nettoyage reseau.
|
||||||
|
// L'appelant normal -- AHarvestableResource -- est deja cote serveur.
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer::GiveItemOrDrop appelee sans autorite pour %s, ignoree."),
|
||||||
|
*GetNameSafe(Item));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Rejected = InventoryComponent->AddItem(Item, Quantity, RemainingUses);
|
||||||
|
if (Rejected > 0)
|
||||||
|
{
|
||||||
|
DropAtFeet(Item, Rejected, RemainingUses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Quantity - Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
void AFpsPlayer::DropAllItems()
|
void AFpsPlayer::DropAllItems()
|
||||||
{
|
{
|
||||||
// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
|
// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "HarvestableDataAsset.h"
|
||||||
|
|
||||||
|
#include "ItemDataAsset.h"
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
#include "Misc/DataValidation.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "HarvestableData"
|
||||||
|
|
||||||
|
int32 FHarvestDrop::Roll() const
|
||||||
|
{
|
||||||
|
if (!Item)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le jet de chance d'abord : une ligne a 30 % qui echoue ne doit pas consommer
|
||||||
|
// de tirage de quantite, sinon deux tables identiques a la chance pres ne
|
||||||
|
// donneraient pas la meme suite -- sans consequence en jeu, mais deroutant a
|
||||||
|
// deboguer avec une graine fixee.
|
||||||
|
if (Chance < 1.f && FMath::FRand() > Chance)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max borne par Min plutot que l'inverse : un designer qui saisit 3 puis 1
|
||||||
|
// obtient 3, jamais un intervalle vide qui rendrait la ligne muette sans rien
|
||||||
|
// dire. RandRange est inclusif des deux cotes.
|
||||||
|
return FMath::RandRange(MinQuantity, FMath::Max(MinQuantity, MaxQuantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
EDataValidationResult UHarvestableDataAsset::IsDataValid(FDataValidationContext& Context) const
|
||||||
|
{
|
||||||
|
EDataValidationResult Result = Super::IsDataValid(Context);
|
||||||
|
|
||||||
|
// Le symptome d'une ligne vide est fourbe : l'arbre se recolte, s'abat, et
|
||||||
|
// rend simplement moins que prevu. Rien dans le log, rien a l'ecran.
|
||||||
|
auto ValidateTable = [&Context, &Result](const TArray<FHarvestDrop>& Table, const FText& TableName)
|
||||||
|
{
|
||||||
|
for (int32 Index = 0; Index < Table.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (!Table[Index].Item)
|
||||||
|
{
|
||||||
|
Context.AddError(FText::Format(
|
||||||
|
LOCTEXT("DropNoItem", "{0}[{1}] has no 'Item' assigned: this line would silently yield nothing."),
|
||||||
|
TableName, FText::AsNumber(Index)));
|
||||||
|
Result = EDataValidationResult::Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ValidateTable(PerHitDrops, LOCTEXT("PerHitTable", "Per Hit Drops"));
|
||||||
|
ValidateTable(DepletionDrops, LOCTEXT("DepletionTable", "Depletion Drops"));
|
||||||
|
|
||||||
|
// Une ressource sans aucun butin est frappable, s'epuise, et ne donne rien.
|
||||||
|
// C'est legitime pour un decor destructible ; ca ne l'est presque jamais ici.
|
||||||
|
if (PerHitDrops.IsEmpty() && DepletionDrops.IsEmpty())
|
||||||
|
{
|
||||||
|
Context.AddWarning(LOCTEXT("NoDropsAtAll", "This harvestable has no drops at all: harvesting it will consume tool durability for nothing."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "HarvestableResource.h"
|
||||||
|
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "FpsPlayer.h"
|
||||||
|
#include "HarvestableDataAsset.h"
|
||||||
|
#include "ItemDataAsset.h"
|
||||||
|
#include "Kismet/GameplayStatics.h"
|
||||||
|
#include "NiagaraFunctionLibrary.h"
|
||||||
|
#include "Net/UnrealNetwork.h"
|
||||||
|
#include "TimerManager.h"
|
||||||
|
|
||||||
|
AHarvestableResource::AHarvestableResource()
|
||||||
|
{
|
||||||
|
// Le Tick ne sert QU'AU balancement, et il s'allume tout seul le temps de
|
||||||
|
// l'amortissement. Une foret de quatre cents arbres qui ticke en permanence
|
||||||
|
// pour ne rien faire est exactement ce qu'on evite ici.
|
||||||
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
PrimaryActorTick.bStartWithTickEnabled = false;
|
||||||
|
|
||||||
|
// Acteur du monde : le compte des coups appartient au serveur. Quatre clients
|
||||||
|
// qui compteraient chacun leurs points de vie divergeraient, et l'arbre
|
||||||
|
// tomberait chez l'un alors qu'il tient encore chez l'autre.
|
||||||
|
bReplicates = true;
|
||||||
|
SetReplicateMovement(false);
|
||||||
|
|
||||||
|
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||||
|
SetRootComponent(Mesh);
|
||||||
|
|
||||||
|
// QueryAndPhysics : l'arbre barre le passage en plus d'etre visable. Le trace
|
||||||
|
// du coup passe par le canal Visibility, que la collision par defaut bloque.
|
||||||
|
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||||
|
|
||||||
|
// Le balancement bouge le mesh a l'execution : Static le cuirait sur place et
|
||||||
|
// la rotation n'aurait aucun effet a l'ecran, sans le moindre avertissement.
|
||||||
|
Mesh->SetMobility(EComponentMobility::Movable);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||||
|
{
|
||||||
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||||
|
|
||||||
|
DOREPLIFETIME(AHarvestableResource, bDepleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
// Retenue ici et pas au constructeur : la rotation relative du mesh peut avoir
|
||||||
|
// ete reglee dans le Blueprint ou sur l'instance posee dans le niveau, et c'est
|
||||||
|
// a CETTE valeur que le balancement doit revenir.
|
||||||
|
BaseMeshRotation = Mesh->GetRelativeRotation();
|
||||||
|
|
||||||
|
// AVANT le premier ApplyDepletionVisuals, qui peut deja poser la souche chez un
|
||||||
|
// client arrivant sur un arbre abattu : on capturerait alors la souche comme
|
||||||
|
// etat intact, et l'arbre ne repousserait jamais.
|
||||||
|
IntactMesh = Mesh->GetStaticMesh();
|
||||||
|
|
||||||
|
if (HasAuthority() && HarvestData)
|
||||||
|
{
|
||||||
|
CurrentHealth = HarvestData->Health;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sur toutes les machines : chez l'hote parce qu'aucun OnRep ne se declenche
|
||||||
|
// pour lui, chez un client parce que l'ordre d'arrivee entre la construction de
|
||||||
|
// l'acteur et ses proprietes repliquees n'est garanti par rien.
|
||||||
|
ApplyDepletionVisuals();
|
||||||
|
|
||||||
|
if (!HarvestData)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("AHarvestableResource : %s n'a pas de HarvestData, il ne se recoltera jamais."),
|
||||||
|
*GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AHarvestableResource::AcceptsTool(const UItemDataAsset* Tool) const
|
||||||
|
{
|
||||||
|
if (!HarvestData)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mains vides = EToolType::None, exactement comme un objet qui n'est pas un
|
||||||
|
// outil : tenir une pomme ne doit pas abattre plus qu'un poing.
|
||||||
|
const EToolType HeldType = (Tool && Tool->IsTool()) ? Tool->ToolType : EToolType::None;
|
||||||
|
|
||||||
|
return HeldType == HarvestData->RequiredTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
EHarvestResult AHarvestableResource::TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||||
|
{
|
||||||
|
// La garde est ici et pas seulement chez l'appelant : c'est le point d'entree
|
||||||
|
// unique du systeme, et rien ne garantit qu'un futur appelant y pense.
|
||||||
|
if (!HasAuthority() || !HarvestData || !Harvester || Power <= 0.f)
|
||||||
|
{
|
||||||
|
return EHarvestResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bDepleted)
|
||||||
|
{
|
||||||
|
// Un « toc » et rien d'autre : le joueur doit entendre que sa souche ne
|
||||||
|
// donne plus rien, sans y perdre de durabilite.
|
||||||
|
Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection);
|
||||||
|
return EHarvestResult::AlreadyDepleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AcceptsTool(Tool))
|
||||||
|
{
|
||||||
|
Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection);
|
||||||
|
return EHarvestResult::WrongTool;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHealth -= Power;
|
||||||
|
|
||||||
|
// Le butin par coup part AVANT le test d'epuisement : le coup qui abat rend
|
||||||
|
// donc les deux tables. C'est ce que le joueur attend -- son dernier coup a
|
||||||
|
// bien entaille le tronc en plus de le faire tomber.
|
||||||
|
GrantDrops(HarvestData->PerHitDrops, Harvester);
|
||||||
|
|
||||||
|
if (CurrentHealth > 0.f)
|
||||||
|
{
|
||||||
|
Multicast_PlayHarvestFeedback(EHarvestFeedback::Hit, ImpactPoint, HitDirection);
|
||||||
|
return EHarvestResult::Harvested;
|
||||||
|
}
|
||||||
|
|
||||||
|
GrantDrops(HarvestData->DepletionDrops, Harvester);
|
||||||
|
Multicast_PlayHarvestFeedback(EHarvestFeedback::FinalBlow, ImpactPoint, HitDirection);
|
||||||
|
Deplete();
|
||||||
|
|
||||||
|
return EHarvestResult::Depleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 AHarvestableResource::GrantDrops(const TArray<FHarvestDrop>& Table, AFpsPlayer* Harvester)
|
||||||
|
{
|
||||||
|
int32 Granted = 0;
|
||||||
|
|
||||||
|
for (const FHarvestDrop& Drop : Table)
|
||||||
|
{
|
||||||
|
const int32 Quantity = Drop.Roll();
|
||||||
|
if (Quantity <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passe par le pawn et pas par son inventaire : ce qui ne rentre pas doit
|
||||||
|
// tomber au sol, et un UInventoryComponent ne sait pas engendrer un acteur.
|
||||||
|
Harvester->GiveItemOrDrop(Drop.Item, Quantity);
|
||||||
|
++Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::Deplete()
|
||||||
|
{
|
||||||
|
bDepleted = true;
|
||||||
|
|
||||||
|
// Chez l'hote aucun OnRep ne se declenche : on applique nous-memes juste apres
|
||||||
|
// avoir ecrit la propriete. Meme schema que SetCharacterAppearance.
|
||||||
|
ApplyDepletionVisuals();
|
||||||
|
|
||||||
|
const float Delay = HarvestData ? HarvestData->RegrowDelay : 0.f;
|
||||||
|
if (Delay <= 0.f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetWorldTimerManager().SetTimer(RegrowTimer, this, &AHarvestableResource::Regrow, Delay, /*bLoop=*/false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::Regrow()
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentHealth = HarvestData ? HarvestData->Health : 0.f;
|
||||||
|
bDepleted = false;
|
||||||
|
ApplyDepletionVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::OnRep_Depleted()
|
||||||
|
{
|
||||||
|
ApplyDepletionVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::ApplyDepletionVisuals()
|
||||||
|
{
|
||||||
|
if (!bDepleted)
|
||||||
|
{
|
||||||
|
SetActorHiddenInGame(false);
|
||||||
|
SetActorEnableCollision(true);
|
||||||
|
|
||||||
|
// IntactMesh peut etre nul le tout premier coup -- BeginPlay ne l'a pas
|
||||||
|
// encore capture -- auquel cas il n'y a de toute facon rien a reposer.
|
||||||
|
if (IntactMesh && Mesh->GetStaticMesh() != IntactMesh)
|
||||||
|
{
|
||||||
|
Mesh->SetStaticMesh(IntactMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DepletedMesh)
|
||||||
|
{
|
||||||
|
Mesh->SetStaticMesh(DepletedMesh);
|
||||||
|
SetActorHiddenInGame(false);
|
||||||
|
|
||||||
|
// Une souche reste solide : on la garde visable, ce qui permet au coup
|
||||||
|
// suivant de rencontrer quelque chose et de sonner « inefficace » plutot
|
||||||
|
// que de traverser dans le vide.
|
||||||
|
SetActorEnableCollision(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sans mesh de remplacement, la ressource disparait entierement. La collision
|
||||||
|
// PART avec elle : la laisser donnerait un mur invisible en pleine foret.
|
||||||
|
SetActorHiddenInGame(true);
|
||||||
|
SetActorEnableCollision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::Multicast_PlayHarvestFeedback_Implementation(EHarvestFeedback Feedback,
|
||||||
|
FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection)
|
||||||
|
{
|
||||||
|
PlayHarvestFeedback(Feedback, ImpactPoint, HitDirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint,
|
||||||
|
const FVector& HitDirection)
|
||||||
|
{
|
||||||
|
if (!HarvestData)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un serveur dedie n'a ni haut-parleur ni ecran : les FX y seraient du travail
|
||||||
|
// pur. En serveur d'ecoute la fenetre de l'hote est un client comme un autre,
|
||||||
|
// donc elle passe cette garde.
|
||||||
|
if (IsNetMode(NM_DedicatedServer))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundBase* Sound = nullptr;
|
||||||
|
UNiagaraSystem* Effect = nullptr;
|
||||||
|
|
||||||
|
switch (Feedback)
|
||||||
|
{
|
||||||
|
case EHarvestFeedback::Hit:
|
||||||
|
Sound = HarvestData->HitSound;
|
||||||
|
Effect = HarvestData->HitEffect;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EHarvestFeedback::FinalBlow:
|
||||||
|
// Le son de chute REMPLACE celui du coup, il ne s'y ajoute pas : deux
|
||||||
|
// impacts sur la meme frame s'entendent comme un doublon, pas comme un
|
||||||
|
// evenement plus fort.
|
||||||
|
Sound = HarvestData->DepletionSound ? HarvestData->DepletionSound.Get() : HarvestData->HitSound.Get();
|
||||||
|
Effect = HarvestData->DepletionEffect ? HarvestData->DepletionEffect.Get() : HarvestData->HitEffect.Get();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EHarvestFeedback::Ineffective:
|
||||||
|
Sound = HarvestData->IneffectiveHitSound;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sound)
|
||||||
|
{
|
||||||
|
UGameplayStatics::PlaySoundAtLocation(this, Sound, ImpactPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Effect)
|
||||||
|
{
|
||||||
|
// Les copeaux repartent VERS le joueur : -HitDirection. Oriente dans le sens
|
||||||
|
// du coup, ils gicleraient dans le tronc et on ne verrait rien.
|
||||||
|
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, Effect, ImpactPoint,
|
||||||
|
(-HitDirection).Rotation());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de balancement sur un coup qui n'a rien fait : une souche qui oscille
|
||||||
|
// sous un coup inefficace annonce exactement le contraire de ce qui se passe.
|
||||||
|
if (Feedback != EHarvestFeedback::Ineffective)
|
||||||
|
{
|
||||||
|
StartShake(HitDirection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::StartShake(const FVector& HitDirection)
|
||||||
|
{
|
||||||
|
if (!HarvestData || HarvestData->HitShakeAngle <= 0.f || HarvestData->HitShakeDuration <= 0.f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// L'axe de rotation est perpendiculaire au coup ET a la verticale : l'arbre
|
||||||
|
// penche DANS le sens ou on l'a frappe. Le produit vectoriel degenere quand on
|
||||||
|
// frappe a la verticale -- de haut en bas sur un rocher -- d'ou le repli.
|
||||||
|
FVector WorldAxis = FVector::CrossProduct(FVector::UpVector, HitDirection);
|
||||||
|
if (!WorldAxis.Normalize())
|
||||||
|
{
|
||||||
|
WorldAxis = GetActorRightVector();
|
||||||
|
}
|
||||||
|
|
||||||
|
// En espace local : le mesh tourne dans le repere de son parent, et un arbre
|
||||||
|
// pose avec un yaw quelconque pencherait de travers si l'axe restait en monde.
|
||||||
|
ShakeAxis = GetActorTransform().InverseTransformVectorNoScale(WorldAxis).GetSafeNormal();
|
||||||
|
|
||||||
|
// Reparti de zero meme si un balancement tournait deja : marteler l'arbre doit
|
||||||
|
// relancer l'oscillation a pleine amplitude, pas l'accumuler.
|
||||||
|
ShakeElapsed = 0.f;
|
||||||
|
SetActorTickEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AHarvestableResource::Tick(float DeltaTime)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaTime);
|
||||||
|
|
||||||
|
if (ShakeElapsed < 0.f || !HarvestData)
|
||||||
|
{
|
||||||
|
SetActorTickEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShakeElapsed += DeltaTime;
|
||||||
|
|
||||||
|
const float Duration = HarvestData->HitShakeDuration;
|
||||||
|
if (ShakeElapsed >= Duration)
|
||||||
|
{
|
||||||
|
// On repose la rotation d'origine EXACTEMENT plutot que de laisser la
|
||||||
|
// derniere valeur calculee : a un cheveu pres, l'arbre resterait penche, et
|
||||||
|
// vingt coups plus tard il pousserait franchement de travers.
|
||||||
|
Mesh->SetRelativeRotation(BaseMeshRotation);
|
||||||
|
ShakeElapsed = -1.f;
|
||||||
|
SetActorTickEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float Alpha = ShakeElapsed / Duration;
|
||||||
|
|
||||||
|
// Amortissement au carre plutot que lineaire : l'oscillation meurt vite, ce qui
|
||||||
|
// donne le poids d'un tronc. Lineaire, elle traine et l'arbre parait mou.
|
||||||
|
const float Damping = FMath::Square(1.f - Alpha);
|
||||||
|
|
||||||
|
const float Angle = HarvestData->HitShakeAngle * Damping
|
||||||
|
* FMath::Sin(2.f * PI * HarvestData->HitShakeFrequency * ShakeElapsed);
|
||||||
|
|
||||||
|
// Composition par quaternion et pas addition de FRotator : additionner deux
|
||||||
|
// rotations d'Euler n'est une composition correcte que par chance, et l'arbre
|
||||||
|
// pose avec un pitch non nul partirait de travers.
|
||||||
|
const FQuat Offset(ShakeAxis, FMath::DegreesToRadians(Angle));
|
||||||
|
Mesh->SetRelativeRotation(Offset * BaseMeshRotation.Quaternion());
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "HarvestingComponent.h"
|
||||||
|
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "FpsPlayer.h"
|
||||||
|
#include "GameFramework/Controller.h"
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "HarvestableResource.h"
|
||||||
|
#include "ItemDataAsset.h"
|
||||||
|
|
||||||
|
UHarvestingComponent::UHarvestingComponent()
|
||||||
|
{
|
||||||
|
// Aucun Tick : ce composant ne travaille qu'au moment du coup, une poignee de
|
||||||
|
// fois par minute. C'est toute la difference avec UInteractionComponent, qui
|
||||||
|
// doit entretenir un prompt a l'ecran.
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
// Pas de SetIsReplicatedByDefault : aucune RPC ne vit ici, elles sont sur le
|
||||||
|
// pawn. Voir le commentaire de classe.
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UHarvestingComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
|
||||||
|
{
|
||||||
|
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||||
|
if (!OwnerPawn)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Par le controller et pas par la camera : GetPlayerViewPoint renvoie
|
||||||
|
// exactement ce que le joueur a a l'ecran, secousses et balancement de tete
|
||||||
|
// compris. Le coup part donc pile du centre du viseur.
|
||||||
|
if (const AController* OwnerController = OwnerPawn->GetController())
|
||||||
|
{
|
||||||
|
OwnerController->GetPlayerViewPoint(OutLocation, OutRotation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AActor* UHarvestingComponent::TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const
|
||||||
|
{
|
||||||
|
OutImpactPoint = FVector::ZeroVector;
|
||||||
|
OutDirection = FVector::ForwardVector;
|
||||||
|
|
||||||
|
FVector ViewLocation;
|
||||||
|
FRotator ViewRotation;
|
||||||
|
if (!GetViewPoint(ViewLocation, ViewRotation))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutDirection = ViewRotation.Vector();
|
||||||
|
const FVector TraceEnd = ViewLocation + OutDirection * SwingRange;
|
||||||
|
|
||||||
|
FCollisionQueryParams Params(SCENE_QUERY_STAT(HarvestSwingTrace), /*bTraceComplex=*/false, GetOwner());
|
||||||
|
|
||||||
|
FHitResult Hit;
|
||||||
|
const bool bHit = GetWorld()->SweepSingleByChannel(
|
||||||
|
Hit,
|
||||||
|
ViewLocation,
|
||||||
|
TraceEnd,
|
||||||
|
FQuat::Identity,
|
||||||
|
SwingChannel,
|
||||||
|
FCollisionShape::MakeSphere(SwingRadius),
|
||||||
|
Params);
|
||||||
|
|
||||||
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
if (bDrawDebug)
|
||||||
|
{
|
||||||
|
const FColor Color = bHit ? FColor::Green : FColor::Red;
|
||||||
|
DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, 1.5f, 0, 1.f);
|
||||||
|
if (bHit)
|
||||||
|
{
|
||||||
|
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, SwingRadius, 12, Color, false, 1.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!bHit)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImpactPoint et pas Location : Location est le centre de la sphere balayee a
|
||||||
|
// l'arret, donc a SwingRadius de la surface. Les copeaux en jailliraient a
|
||||||
|
// vingt-cinq centimetres du tronc, en plein air.
|
||||||
|
OutImpactPoint = Hit.ImpactPoint;
|
||||||
|
|
||||||
|
return Hit.GetActor();
|
||||||
|
}
|
||||||
|
|
||||||
|
float UHarvestingComponent::GetSwingPower(const UItemDataAsset* Tool) const
|
||||||
|
{
|
||||||
|
// Un objet qui n'est pas un outil frappe comme un poing : tenir une pomme ne
|
||||||
|
// doit ni empecher de cueillir une baie, ni aider a abattre un arbre.
|
||||||
|
return (Tool && Tool->IsTool()) ? Tool->HarvestPower : UnarmedHarvestPower;
|
||||||
|
}
|
||||||
|
|
||||||
|
EHarvestResult UHarvestingComponent::ApplySwing(AActor* Target, const UItemDataAsset* Tool,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||||
|
{
|
||||||
|
AActor* Owner = GetOwner();
|
||||||
|
if (!Owner || !Owner->HasAuthority())
|
||||||
|
{
|
||||||
|
return EHarvestResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
AHarvestableResource* Resource = Cast<AHarvestableResource>(Target);
|
||||||
|
if (!IsValid(Resource))
|
||||||
|
{
|
||||||
|
// Le cas normal, et de loin : la plupart des coups tombent sur le sol, un
|
||||||
|
// mur, ou rien du tout. Ce n'est pas une erreur, juste un coup dans le vide.
|
||||||
|
return EHarvestResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le client a vise il y a un aller-retour : entre-temps il a bouge, et la
|
||||||
|
// position que le serveur connait de lui est en retard. On revalide donc, avec
|
||||||
|
// la meme portee elargie par ServerRangeTolerance.
|
||||||
|
//
|
||||||
|
// La distance se mesure a la BOITE ENGLOBANTE de la cible et pas a son origine :
|
||||||
|
// le pivot d'un arbre de pack est au SOL, donc frapper le tronc a deux metres de
|
||||||
|
// haut mesurerait une distance bien plus grande que la portee du coup. Le
|
||||||
|
// symptome serait un grand arbre impossible a abattre pour un client, et
|
||||||
|
// parfaitement normal chez l'hote.
|
||||||
|
const FBox TargetBounds = Resource->GetComponentsBoundingBox(/*bNonColliding=*/true);
|
||||||
|
const float MaxDistanceSq = FMath::Square(SwingRange * ServerRangeTolerance);
|
||||||
|
|
||||||
|
if (TargetBounds.ComputeSquaredDistanceToPoint(Owner->GetActorLocation()) > MaxDistanceSq)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Verbose, TEXT("UHarvestingComponent : %s a frappe %s, hors de portee cote serveur."),
|
||||||
|
*GetNameSafe(Owner), *GetNameSafe(Resource));
|
||||||
|
return EHarvestResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
AFpsPlayer* Harvester = Cast<AFpsPlayer>(Owner);
|
||||||
|
if (!Harvester)
|
||||||
|
{
|
||||||
|
return EHarvestResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Resource->TryHarvest(Harvester, Tool, GetSwingPower(Tool), ImpactPoint, HitDirection);
|
||||||
|
}
|
||||||
@@ -3,6 +3,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
// FVector_NetQuantize / FVector_NetQuantizeNormal, dans la signature de
|
||||||
|
// Server_UseItem : un point d'impact au demi-centimetre pres et une direction
|
||||||
|
// deja normalisee tiennent en quelques octets la ou deux FVector pleins en
|
||||||
|
// coutent vingt-quatre, pour une precision dont personne ne verrait la difference
|
||||||
|
// sur un jet de copeaux.
|
||||||
|
#include "Engine/NetSerialization.h"
|
||||||
#include "GameFramework/Character.h"
|
#include "GameFramework/Character.h"
|
||||||
#include "FpsPlayer.generated.h"
|
#include "FpsPlayer.generated.h"
|
||||||
|
|
||||||
@@ -12,6 +18,7 @@ class UCameraComponent;
|
|||||||
class UCharacterAppearanceComponent;
|
class UCharacterAppearanceComponent;
|
||||||
class UCraftingComponent;
|
class UCraftingComponent;
|
||||||
class UFootstepComponent;
|
class UFootstepComponent;
|
||||||
|
class UHarvestingComponent;
|
||||||
class UHeadBobComponent;
|
class UHeadBobComponent;
|
||||||
class UHeldItemComponent;
|
class UHeldItemComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
@@ -167,6 +174,9 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Audio")
|
UFUNCTION(BlueprintPure, Category = "Audio")
|
||||||
UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; }
|
UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvesting")
|
||||||
|
UHarvestingComponent* GetHarvestingComponent() const { return HarvestingComponent; }
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Commandes console d'apparence
|
// Commandes console d'apparence
|
||||||
//
|
//
|
||||||
@@ -245,6 +255,23 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Inventaire")
|
UFUNCTION(BlueprintCallable, Category = "Inventaire")
|
||||||
void DropAllItems();
|
void DropAllItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Donne un objet au joueur, et pose au sol ce qui ne rentre pas.
|
||||||
|
*
|
||||||
|
* Le chemin normal de tout ce que le monde produit : récolte, surplus de
|
||||||
|
* fabrication, futur butin. Refuser parce que l'inventaire est plein ferait
|
||||||
|
* disparaître ce que le joueur vient d'obtenir sous ses yeux — c'est le
|
||||||
|
* genre de perte silencieuse qui coûte une session de jeu.
|
||||||
|
*
|
||||||
|
* Passe par `AddItem`, donc exactement comme un ramassage : piles entamées
|
||||||
|
* d'abord, puis barre rapide, puis grille.
|
||||||
|
*
|
||||||
|
* @return le nombre d'exemplaires réellement entrés dans l'inventaire ; le
|
||||||
|
* reste est au sol.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Inventaire")
|
||||||
|
int32 GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses = -1);
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Survie")
|
UFUNCTION(BlueprintPure, Category = "Survie")
|
||||||
bool IsDead() const { return bIsDead; }
|
bool IsDead() const { return bIsDead; }
|
||||||
|
|
||||||
@@ -472,6 +499,16 @@ protected:
|
|||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio")
|
||||||
TObjectPtr<UFootstepComponent> FootstepComponent;
|
TObjectPtr<UFootstepComponent> FootstepComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le coup d'outil : trace du geste et recolte.
|
||||||
|
*
|
||||||
|
* Sur le pawn parce qu'il frappe avec CE corps, depuis CETTE camera, et qu'un
|
||||||
|
* cadavre ne recolte plus. Pendant exact d'UInteractionComponent, dont il
|
||||||
|
* partage le schema reseau -- voir son commentaire de classe.
|
||||||
|
*/
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvesting")
|
||||||
|
TObjectPtr<UHarvestingComponent> HarvestingComponent;
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Enhanced Input : assets à assigner dans le Blueprint
|
// Enhanced Input : assets à assigner dans le Blueprint
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
@@ -786,12 +823,34 @@ private:
|
|||||||
* Le slot est passé explicitement : SelectedHotbarIndex n'est PAS répliqué,
|
* Le slot est passé explicitement : SelectedHotbarIndex n'est PAS répliqué,
|
||||||
* c'est une intention purement locale. Le serveur ne peut donc pas deviner
|
* c'est une intention purement locale. Le serveur ne peut donc pas deviner
|
||||||
* ce que le joueur avait en main.
|
* ce que le joueur avait en main.
|
||||||
|
*
|
||||||
|
* La CIBLE aussi voyage, pour la même raison de fond : elle a été tracée
|
||||||
|
* depuis le point de vue réel du joueur à l'instant du geste. La retracer
|
||||||
|
* côté serveur la retracerait depuis une position qui a un demi-aller-retour
|
||||||
|
* de retard — un joueur en mouvement raterait des arbres qu'il vise pourtant
|
||||||
|
* en plein. Le serveur revalide la distance, il ne refait pas la visée.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(Server, Reliable)
|
UFUNCTION(Server, Reliable)
|
||||||
void Server_UseItem(int32 SlotIndex);
|
void Server_UseItem(int32 SlotIndex, AActor* SwingTarget, FVector_NetQuantize ImpactPoint,
|
||||||
|
FVector_NetQuantizeNormal HitDirection);
|
||||||
|
|
||||||
/** L'utilisation réelle, côté autorité. */
|
/** L'utilisation réelle, côté autorité. */
|
||||||
void PerformUseItem(int32 SlotIndex);
|
void PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint,
|
||||||
|
const FVector& HitDirection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le coup d'outil côté autorité, une fois la cible validée.
|
||||||
|
*
|
||||||
|
* Séparée de `PerformUseItem` parce qu'elle porte une décision qui lui est
|
||||||
|
* propre : l'usure. Un coup qui ne porte pas — mauvais outil, souche, vide —
|
||||||
|
* ne consomme aucune charge, sinon taper un rocher à la hache l'abîmerait
|
||||||
|
* sans rien rendre.
|
||||||
|
*
|
||||||
|
* @param SlotIndex la case de l'outil, pour y prélever la charge. INDEX_NONE
|
||||||
|
* à mains nues.
|
||||||
|
*/
|
||||||
|
void PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Point d'entrée unique du sprint : applique en local ET prévient le serveur.
|
* Point d'entrée unique du sprint : applique en local ET prévient le serveur.
|
||||||
@@ -839,6 +898,13 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
|
void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pose une pile aux pieds du joueur. Le fond commun de `HandleCraftOverflow`
|
||||||
|
* et de `GiveItemOrDrop` : deux copies finiraient par diverger, et l'une des
|
||||||
|
* deux oublierait le trace vers le sol.
|
||||||
|
*/
|
||||||
|
void DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
|
||||||
|
|
||||||
/** Fait apparaitre un pickup deja configure a un endroit donne. */
|
/** Fait apparaitre un pickup deja configure a un endroit donne. */
|
||||||
APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location);
|
APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "ItemDataAsset.h"
|
||||||
|
#include "HarvestableDataAsset.generated.h"
|
||||||
|
|
||||||
|
class UItemDataAsset;
|
||||||
|
class UNiagaraSystem;
|
||||||
|
class USoundBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Une ligne de butin : « 1 a 3 Bois, 70 % du temps ».
|
||||||
|
*
|
||||||
|
* Le tirage n'a lieu QUE sur le serveur (AHarvestableResource::TryHarvest), donc
|
||||||
|
* l'aleatoire ne peut pas diverger d'une machine a l'autre -- les clients ne
|
||||||
|
* recoivent jamais le jet, seulement l'objet qui atterrit dans l'inventaire.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FHarvestDrop
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop")
|
||||||
|
TObjectPtr<UItemDataAsset> Item;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1"))
|
||||||
|
int32 MinQuantity = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1"))
|
||||||
|
int32 MaxQuantity = 1;
|
||||||
|
|
||||||
|
/** Probabilite que cette ligne tombe. 1 = toujours. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "0", ClampMax = "1"))
|
||||||
|
float Chance = 1.f;
|
||||||
|
|
||||||
|
/** Tire une quantite pour ce coup. Renvoie 0 quand le jet echoue. */
|
||||||
|
int32 Roll() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les REGLES de recolte d'une ressource : ce qu'il faut pour l'abattre, ce
|
||||||
|
* qu'elle rend, en combien de temps elle repousse.
|
||||||
|
*
|
||||||
|
* Un asset par ESPECE, pas par instance ni par mesh : les quinze variantes de
|
||||||
|
* sapin du pack partagent toutes DA_Harvest_Tree, chacune posant son propre mesh
|
||||||
|
* sur son acteur. C'est la meme separation que UItemDataAsset et APickupItem --
|
||||||
|
* la donnee decrit une categorie, l'acteur porte le visuel et l'etat.
|
||||||
|
*
|
||||||
|
* Ce qui ne doit JAMAIS venir ici : les points de vie restants, le fait qu'un
|
||||||
|
* arbre soit deja abattu, l'heure de sa repousse. C'est de l'etat, il vit sur
|
||||||
|
* AHarvestableResource -- sinon abattre un arbre abattrait toute la foret.
|
||||||
|
*/
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class EMBERWILD_API UHarvestableDataAsset : public UPrimaryDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Identifiant stable, pour la sauvegarde a venir. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
FName HarvestableId;
|
||||||
|
|
||||||
|
/** Nom lisible, pour les logs et un futur message « Il vous faut une hache ». */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
FText DisplayName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outil capable d'entamer cette ressource.
|
||||||
|
*
|
||||||
|
* `None` ne veut pas dire « n'importe quoi » mais « a mains nues » : c'est ce
|
||||||
|
* qui rend un buisson de baies cueillable au poing, sans code particulier.
|
||||||
|
* Un arbre demande Axe, un filon Pickaxe.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
EToolType RequiredTool = EToolType::Axe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points a retirer avant epuisement.
|
||||||
|
*
|
||||||
|
* En points et pas en nombre de coups, pour que la hache en fer abatte en
|
||||||
|
* quatre coups l'arbre qui en demande dix a la hache de pierre : c'est
|
||||||
|
* `HarvestPower` de l'outil qui fait la difference. Un compte de coups en dur
|
||||||
|
* ici obligerait a dupliquer chaque ressource par palier d'outil.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "1"))
|
||||||
|
float Health = 100.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce que rend CHAQUE coup, meme celui qui n'abat pas.
|
||||||
|
*
|
||||||
|
* C'est la moitie qui rend le geste satisfaisant : un arbre qui ne donne rien
|
||||||
|
* pendant neuf coups laisse croire qu'on tape dans le vide. Garde les
|
||||||
|
* quantites basses -- c'est le lot final qui doit peser.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
TArray<FHarvestDrop> PerHitDrops;
|
||||||
|
|
||||||
|
/** Bonus du coup qui epuise la ressource. C'est la recompense de l'abattage. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
TArray<FHarvestDrop> DepletionDrops;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secondes avant que la ressource redevienne recoltable. 0 = jamais.
|
||||||
|
*
|
||||||
|
* Une souche qui repousse dispense d'un systeme de spawn tant que la carte
|
||||||
|
* est peuplee a la main : rien ne disparait, donc rien n'a a etre recree.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "0", Units = "s"))
|
||||||
|
float RegrowDelay = 180.f;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Retour au joueur
|
||||||
|
//
|
||||||
|
// Sur la DONNEE et non sur l'acteur : c'est l'espece qui decide qu'un coup
|
||||||
|
// de hache dans du bois claque et qu'un coup de pioche dans la pierre
|
||||||
|
// resonne. Regler ca instance par instance sur quatre cents arbres poses
|
||||||
|
// dans le niveau serait ingerable.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Joue a chaque coup qui entame reellement la ressource. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
|
||||||
|
TObjectPtr<USoundBase> HitSound;
|
||||||
|
|
||||||
|
/** Copeaux, eclats de pierre. Joue au point d'impact, oriente par la normale. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
|
||||||
|
TObjectPtr<UNiagaraSystem> HitEffect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le coup n'a rien fait : mauvais outil, ou ressource deja epuisee.
|
||||||
|
*
|
||||||
|
* Un seul son pour les deux cas, et c'est volontaire : dans les deux le
|
||||||
|
* joueur doit entendre que son geste ne sert a rien. Le laisser vide donne un
|
||||||
|
* coup parfaitement muet, ce que l'oreille lit comme un bug.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
|
||||||
|
TObjectPtr<USoundBase> IneffectiveHitSound;
|
||||||
|
|
||||||
|
/** Craquement de chute, eboulement. Joue sur le coup qui epuise. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
|
||||||
|
TObjectPtr<USoundBase> DepletionSound;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
|
||||||
|
TObjectPtr<UNiagaraSystem> DepletionEffect;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angle maximal du balancement encaisse a chaque coup, en degres.
|
||||||
|
*
|
||||||
|
* 0 pour un rocher : la pierre ne plie pas, et la voir osciller casse
|
||||||
|
* immediatement l'illusion de masse. Quelques degres suffisent sur un arbre --
|
||||||
|
* au-dela le tronc a l'air en caoutchouc.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback",
|
||||||
|
meta = (ClampMin = "0", ClampMax = "45", Units = "deg"))
|
||||||
|
float HitShakeAngle = 3.f;
|
||||||
|
|
||||||
|
/** Duree du balancement amorti, en secondes. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback",
|
||||||
|
meta = (ClampMin = "0", Units = "s"))
|
||||||
|
float HitShakeDuration = 0.45f;
|
||||||
|
|
||||||
|
/** Oscillations par seconde. Plus haut = plus nerveux, plus leger. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback", meta = (ClampMin = "0.1"))
|
||||||
|
float HitShakeFrequency = 7.f;
|
||||||
|
|
||||||
|
/** Vrai si cette ressource se recolte a mains nues. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvestable")
|
||||||
|
bool IsHarvestableByHand() const { return RequiredTool == EToolType::None; }
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
/**
|
||||||
|
* Une table de butin dont une ligne pointe dans le vide ne planterait pas :
|
||||||
|
* l'arbre donnerait simplement moins, sans rien dire. C'est le genre de bug
|
||||||
|
* qu'on cherche une heure -- autant que l'editeur le refuse a la sauvegarde.
|
||||||
|
*/
|
||||||
|
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/NetSerialization.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "HarvestableResource.generated.h"
|
||||||
|
|
||||||
|
class AFpsPlayer;
|
||||||
|
class UHarvestableDataAsset;
|
||||||
|
class UItemDataAsset;
|
||||||
|
class UStaticMesh;
|
||||||
|
class UStaticMeshComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce qu'a produit un coup, du point de vue de celui qui frappe.
|
||||||
|
*
|
||||||
|
* Un enum et pas un booleen, parce que l'appelant en tire trois conduites
|
||||||
|
* distinctes : un coup qui porte use l'outil et rend du butin, un mauvais outil
|
||||||
|
* ne doit RIEN user -- sinon taper un rocher a la hache la deteriore sans rien
|
||||||
|
* donner, ce que personne ne peut comprendre -- et une ressource deja epuisee
|
||||||
|
* n'est pas une erreur du joueur, juste un geste sans effet.
|
||||||
|
*/
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EHarvestResult : uint8
|
||||||
|
{
|
||||||
|
/** Le coup a porte : du butin est parti, l'outil s'use. */
|
||||||
|
Harvested,
|
||||||
|
|
||||||
|
/** Le coup a epuise la ressource : butin final compris. */
|
||||||
|
Depleted,
|
||||||
|
|
||||||
|
/** L'outil en main ne peut rien contre cette ressource. */
|
||||||
|
WrongTool,
|
||||||
|
|
||||||
|
/** Deja epuisee ; elle repoussera peut-etre. */
|
||||||
|
AlreadyDepleted,
|
||||||
|
|
||||||
|
/** Cible invalide, donnee manquante : rien ne s'est produit. */
|
||||||
|
Invalid
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ce que le multicast de retour doit jouer. Un seul RPC pour les trois cas. */
|
||||||
|
UENUM()
|
||||||
|
enum class EHarvestFeedback : uint8
|
||||||
|
{
|
||||||
|
Hit,
|
||||||
|
FinalBlow,
|
||||||
|
Ineffective
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un arbre, un rocher, un buisson : quelque chose qu'on frappe pour en tirer des
|
||||||
|
* ressources.
|
||||||
|
*
|
||||||
|
* Ce n'est PAS un IInteractable, et c'est la decision structurante : on ne
|
||||||
|
* « valide » pas un arbre, on le frappe. Le coup part du marqueur
|
||||||
|
* UAnimNotify_ItemAction pose dans le montage de l'outil, donc au moment ou la
|
||||||
|
* hache touche -- jamais a l'appui. L'acteur n'a donc ni prompt, ni volume de
|
||||||
|
* visee dedie : le trace du coup vient toucher son MESH, comme un vrai coup.
|
||||||
|
*
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* Ou vit quoi
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Les REGLES (outil requis, points de vie, butin, repousse) sont dans le
|
||||||
|
* UHarvestableDataAsset, partage par toute l'espece. Les MESHES sont ici, sur
|
||||||
|
* l'acteur : les quinze sapins du pack ont quinze visuels et une seule regle.
|
||||||
|
* Mettre le mesh sur la donnee obligerait a un DataAsset par variante, mettre la
|
||||||
|
* regle sur l'acteur obligerait a re-regler le butin sur quatre cents instances.
|
||||||
|
*
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* Reseau : le serveur compte, les clients regardent
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* CurrentHealth n'est PAS repliquee et ne doit jamais l'etre : c'est un compteur
|
||||||
|
* qui change a chaque coup pour un resultat que le joueur lit deja dans le
|
||||||
|
* balancement du tronc et dans les objets qui arrivent dans son sac. Seul
|
||||||
|
* bDepleted part sur le reseau -- une transition, pas un flux.
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class EMBERWILD_API AHarvestableResource : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AHarvestableResource();
|
||||||
|
|
||||||
|
virtual void Tick(float DeltaTime) override;
|
||||||
|
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encaisse un coup. A n'appeler que sur le SERVEUR.
|
||||||
|
*
|
||||||
|
* @param Harvester celui qui frappe ; c'est son inventaire qui recoit.
|
||||||
|
* @param Tool l'objet en main, nul si les mains sont vides.
|
||||||
|
* @param Power points retires par ce coup.
|
||||||
|
* @param ImpactPoint point touche par le trace, pour les copeaux.
|
||||||
|
* @param HitDirection direction du coup, normalisee, pour le sens du balancement.
|
||||||
|
*/
|
||||||
|
EHarvestResult TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection);
|
||||||
|
|
||||||
|
/** Vrai si l'outil fourni (nul = mains nues) peut entamer cette ressource. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvestable")
|
||||||
|
bool AcceptsTool(const UItemDataAsset* Tool) const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvestable")
|
||||||
|
bool IsDepleted() const { return bDepleted; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvestable")
|
||||||
|
UHarvestableDataAsset* GetHarvestData() const { return HarvestData; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le visuel ET la surface qu'on frappe.
|
||||||
|
*
|
||||||
|
* Le mesh porte lui-meme la collision, comme ACraftingStation et contrairement
|
||||||
|
* a APickupItem : un arbre est un obstacle, il a forcement une collision, et un
|
||||||
|
* volume ajoute par-dessus laisserait frapper le vide autour du tronc.
|
||||||
|
* Corollaire a verifier sur chaque mesh de pack : sans collision simple, le
|
||||||
|
* trace du coup ne rencontre rien et l'arbre est intouchable.
|
||||||
|
*/
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||||
|
|
||||||
|
/** Les regles de recolte. Sans elle l'acteur est un simple decor. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
TObjectPtr<UHarvestableDataAsset> HarvestData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ce qu'on voit une fois la ressource epuisee : la souche, le filon vide.
|
||||||
|
*
|
||||||
|
* Vide = l'acteur se cache entierement, collision comprise. C'est un repli
|
||||||
|
* acceptable pour un buisson de baies -- personne ne remarque un buisson
|
||||||
|
* absent -- mais un arbre qui s'evapore se voit immediatement : donne-lui une
|
||||||
|
* souche.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
TObjectPtr<UStaticMesh> DepletedMesh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Epuisee, en attente de repousse.
|
||||||
|
*
|
||||||
|
* ReplicatedUsing et pas simplement Replicated : c'est le meme piege que
|
||||||
|
* APickupItem::ItemData. Un client qui rejoint une partie en cours construit
|
||||||
|
* l'acteur AVANT que la propriete arrive -- il verrait l'arbre intact et
|
||||||
|
* pourrait le frapper, alors que le serveur le sait deja abattu.
|
||||||
|
*/
|
||||||
|
UPROPERTY(ReplicatedUsing = OnRep_Depleted, BlueprintReadOnly, Category = "Harvestable")
|
||||||
|
bool bDepleted = false;
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void OnRep_Depleted();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Points restants. SERVEUR UNIQUEMENT -- voir le commentaire de classe.
|
||||||
|
* Initialisee au BeginPlay depuis la donnee, et remise a plein a la repousse.
|
||||||
|
*/
|
||||||
|
float CurrentHealth = 0.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le mesh d'origine, retenu au BeginPlay pour le reposer a la repousse.
|
||||||
|
*
|
||||||
|
* Retenu et surtout pas relu depuis le CDO de la classe : une instance posee
|
||||||
|
* dans le niveau peut avoir son propre mesh -- c'est meme le cas normal, un
|
||||||
|
* seul BP_Harvestable servant les quinze variantes d'arbre du pack. Le CDO
|
||||||
|
* rendrait alors le mesh du Blueprint, et l'arbre repousserait en sapin
|
||||||
|
* generique. Le symptome n'apparait qu'apres RegrowDelay secondes.
|
||||||
|
*/
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<UStaticMesh> IntactMesh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verse le contenu d'une table dans l'inventaire du recolteur.
|
||||||
|
* @return le nombre de lignes qui ont reellement donne quelque chose.
|
||||||
|
*/
|
||||||
|
int32 GrantDrops(const TArray<struct FHarvestDrop>& Table, AFpsPlayer* Harvester);
|
||||||
|
|
||||||
|
/** Passe la ressource a l'etat epuise et arme la repousse. Serveur. */
|
||||||
|
void Deplete();
|
||||||
|
|
||||||
|
/** Remet la ressource d'aplomb. Appelee par le timer, donc serveur. */
|
||||||
|
void Regrow();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pose le mesh correspondant a bDepleted. Tourne sur TOUTES les machines.
|
||||||
|
*
|
||||||
|
* Partagee par BeginPlay et OnRep : deux copies finiraient par diverger, et le
|
||||||
|
* symptome serait une souche correcte chez l'hote et un arbre intact chez le
|
||||||
|
* client qui vient de le voir tomber.
|
||||||
|
*/
|
||||||
|
void ApplyDepletionVisuals();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retour audiovisuel du coup, joue chez tout le monde.
|
||||||
|
*
|
||||||
|
* Unreliable, et c'est un choix : un paquet perdu coute un bruit de hache, pas
|
||||||
|
* un etat de jeu. Le fiabiliser ferait payer un accuse de reception a chacun
|
||||||
|
* des trois autres joueurs pour chaque coup porte dans la partie.
|
||||||
|
*/
|
||||||
|
UFUNCTION(NetMulticast, Unreliable)
|
||||||
|
void Multicast_PlayHarvestFeedback(EHarvestFeedback Feedback, FVector_NetQuantize ImpactPoint,
|
||||||
|
FVector_NetQuantizeNormal HitDirection);
|
||||||
|
|
||||||
|
/** Joue son, particules et balancement. Locale a chaque machine. */
|
||||||
|
void PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint, const FVector& HitDirection);
|
||||||
|
|
||||||
|
/** Lance le balancement. Axe deduit de la direction du coup. */
|
||||||
|
void StartShake(const FVector& HitDirection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temps ecoule dans le balancement en cours, en secondes.
|
||||||
|
* Negatif = aucun balancement, donc le Tick est coupe.
|
||||||
|
*/
|
||||||
|
float ShakeElapsed = -1.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Axe autour duquel le mesh oscille, en espace LOCAL.
|
||||||
|
*
|
||||||
|
* En local et pas en monde : le mesh tourne dans le repere de son parent, et
|
||||||
|
* un arbre pose avec un yaw quelconque ferait pencher le tronc de travers si
|
||||||
|
* l'axe restait exprime en monde.
|
||||||
|
*/
|
||||||
|
FVector ShakeAxis = FVector::RightVector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotation d'origine du mesh, pour y revenir exactement.
|
||||||
|
*
|
||||||
|
* Retenue et jamais recalculee : accumuler des AddRelativeRotation laisse
|
||||||
|
* derriver l'arbre a chaque coup, et au bout de vingt coups il pousse
|
||||||
|
* franchement de travers. Le symptome ne sort qu'apres une longue session.
|
||||||
|
*/
|
||||||
|
FRotator BaseMeshRotation = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
/** Repousse, cote serveur uniquement. */
|
||||||
|
FTimerHandle RegrowTimer;
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Engine/EngineTypes.h"
|
||||||
|
#include "HarvestableResource.h"
|
||||||
|
#include "HarvestingComponent.generated.h"
|
||||||
|
|
||||||
|
class UItemDataAsset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le COUP : ce qui part quand la hache touche.
|
||||||
|
*
|
||||||
|
* A poser sur le pawn du joueur, a cote d'UInteractionComponent -- dont il est le
|
||||||
|
* pendant exact, et la comparaison est la meilleure facon de comprendre les deux :
|
||||||
|
*
|
||||||
|
* UInteractionComponent trace en continu, affiche un prompt, agit sur E.
|
||||||
|
* UHarvestingComponent ne trace QU'AU moment du coup, n'affiche rien.
|
||||||
|
*
|
||||||
|
* On ne « vise » pas un arbre, on le frappe : il n'y a donc ni cible memorisee,
|
||||||
|
* ni surbrillance, ni texte a l'ecran. Le trace part une fois par coup, depuis
|
||||||
|
* l'AnimNotify pose dans le montage de l'outil -- jamais a l'appui du clic, sans
|
||||||
|
* quoi la hache abattrait l'arbre avant de l'avoir touche.
|
||||||
|
*
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* Reseau : le client vise, le serveur tranche
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* C'est le schema d'UInteractionComponent, et pour la meme raison : le trace doit
|
||||||
|
* partir du point de vue REEL du joueur au moment du geste. Le refaire cote
|
||||||
|
* serveur le referait depuis une position qui a un demi-aller-retour de retard,
|
||||||
|
* et un joueur en mouvement raterait des arbres qu'il vise pourtant en plein.
|
||||||
|
*
|
||||||
|
* La cible part donc dans la RPC (AFpsPlayer::Server_UseItem) et le serveur
|
||||||
|
* revalide la distance avec une tolerance. Ce n'est PAS de l'anti-triche -- coop
|
||||||
|
* entre amis -- mais un garde-fou contre un client desynchronise qui abattrait
|
||||||
|
* un arbre a l'autre bout de la carte.
|
||||||
|
*
|
||||||
|
* Aucune RPC ne vit sur ce composant : elles sont toutes sur le pawn, qui portait
|
||||||
|
* deja Server_UseItem. Un composant qui en porterait aurait besoin de
|
||||||
|
* SetIsReplicatedByDefault(true), sans quoi le moteur les jetterait en silence.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||||
|
class EMBERWILD_API UHarvestingComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UHarvestingComponent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace le coup depuis le point de vue du joueur. A appeler EN LOCAL.
|
||||||
|
*
|
||||||
|
* Renvoie n'importe quel acteur touche, pas seulement une ressource : c'est le
|
||||||
|
* serveur qui decide de ce qu'on peut recolter, et lui renvoyer un nullptr
|
||||||
|
* parce qu'on a mal identifie la cible chez nous serait lui retirer ce mot.
|
||||||
|
*
|
||||||
|
* @param OutImpactPoint point touche, pour les copeaux et la validation.
|
||||||
|
* @param OutDirection direction du coup, normalisee.
|
||||||
|
* @return l'acteur touche, ou nullptr si le coup part dans le vide.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Harvesting")
|
||||||
|
AActor* TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique le coup a la cible. A n'appeler que sur le SERVEUR.
|
||||||
|
*
|
||||||
|
* @param Tool l'objet en main, nul si les mains sont vides.
|
||||||
|
* @return ce qui s'est produit ; l'appelant s'en sert pour decider de l'usure.
|
||||||
|
*/
|
||||||
|
EHarvestResult ApplySwing(AActor* Target, const UItemDataAsset* Tool,
|
||||||
|
const FVector& ImpactPoint, const FVector& HitDirection);
|
||||||
|
|
||||||
|
/** Points retires par un coup avec cet outil, mains vides comprises. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Harvesting")
|
||||||
|
float GetSwingPower(const UItemDataAsset* Tool) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/**
|
||||||
|
* Portee du coup en cm.
|
||||||
|
*
|
||||||
|
* Plus courte que celle de l'interaction, et c'est voulu : ramasser une branche
|
||||||
|
* a bout de bras est un confort, abattre un arbre a la meme distance donnerait
|
||||||
|
* une hache qui frappe le vide. On doit etre AU pied de l'arbre.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "1", Units = "cm"))
|
||||||
|
float SwingRange = 200.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rayon de la sphere balayee.
|
||||||
|
*
|
||||||
|
* Nettement plus large que celui de l'interaction : on ne vise pas un tronc au
|
||||||
|
* pixel pres, on donne un coup de hache. Un rayon serre rend la recolte penible
|
||||||
|
* sans rendre le jeu plus exigeant.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0", Units = "cm"))
|
||||||
|
float SwingRadius = 25.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting")
|
||||||
|
TEnumAsByte<ECollisionChannel> SwingChannel = ECC_Visibility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puissance d'un coup a mains nues.
|
||||||
|
*
|
||||||
|
* Sur le composant et pas sur un DataAsset, pour la raison evidente qu'il n'y a
|
||||||
|
* pas d'objet a qui la demander -- exactement comme UnarmedUseMontage sur
|
||||||
|
* UHeldItemComponent. Elle n'a d'effet que sur les ressources dont
|
||||||
|
* RequiredTool vaut None : cueillir des baies, arracher des herbes.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0"))
|
||||||
|
float UnarmedHarvestPower = 10.f;
|
||||||
|
|
||||||
|
/** Marge de distance accordee par le serveur. Voir le commentaire de classe. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting|Network", meta = (ClampMin = "1.0"))
|
||||||
|
float ServerRangeTolerance = 2.f;
|
||||||
|
|
||||||
|
/** Affiche le trace du coup. Le premier reflexe quand la hache ne touche rien. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting")
|
||||||
|
bool bDrawDebug = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Point de vue reel du joueur, camera comprise. */
|
||||||
|
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
|
||||||
|
};
|
||||||
@@ -240,20 +240,35 @@ public:
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Outil
|
// Outil
|
||||||
//
|
//
|
||||||
// Volontairement reduit au TYPE pour l'instant : la puissance de recolte et
|
// L'usure passe par MaxUses / RemainingUses, comme une canette : un outil neuf
|
||||||
// le cout en durabilite d'un coup n'ont pas encore de lecteur, et inventer
|
// part a MaxUses charges, l'inventaire les suit deja au sol comme dans un
|
||||||
// leur forme avant d'ecrire les ressources recoltables serait deviner.
|
// coffre, et WBP_ItemDetails affiche deja la barre correspondante. Un coup qui
|
||||||
|
// PORTE consomme une charge -- un coup dans le vide ou contre la mauvaise
|
||||||
|
// matiere n'use rien, voir UHarvestingComponent.
|
||||||
//
|
//
|
||||||
// L'usure, elle, est deja tranchee : ce sera MaxUses / RemainingUses, comme
|
// Corollaire a connaitre : un outil laisse a MaxUses = 1 casserait au premier
|
||||||
// une canette. Un outil neuf part donc a MaxUses charges, l'inventaire les
|
// coup. AFpsPlayer::PerformUseItem s'y refuse et le dit dans le log, plutot que
|
||||||
// suit deja au sol comme dans un coffre, et WBP_ItemDetails affiche deja la
|
// de faire disparaitre la hache que le joueur vient de fabriquer.
|
||||||
// barre correspondante -- rien a ajouter le jour venu.
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
/** Ce que cet objet sait recolter. None = ce n'est pas un outil. */
|
/** Ce que cet objet sait recolter. None = ce n'est pas un outil. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool")
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool")
|
||||||
EToolType ToolType = EToolType::None;
|
EToolType ToolType = EToolType::None;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points retires a une ressource par coup.
|
||||||
|
*
|
||||||
|
* A comparer au Health de UHarvestableDataAsset : 25 contre un arbre a 100
|
||||||
|
* donne quatre coups. C'est ce champ, et lui seul, qui fait la difference entre
|
||||||
|
* la hache de pierre et celle en fer -- la ressource, elle, ne change pas.
|
||||||
|
*
|
||||||
|
* Sans effet sur une ressource que ToolType n'autorise pas : une pioche a 100
|
||||||
|
* de puissance ne coupe toujours pas un arbre.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool",
|
||||||
|
meta = (EditCondition = "ToolType != EToolType::None", ClampMin = "0"))
|
||||||
|
float HarvestPower = 25.f;
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Item|Tool")
|
UFUNCTION(BlueprintPure, Category = "Item|Tool")
|
||||||
bool IsTool() const { return ToolType != EToolType::None; }
|
bool IsTool() const { return ToolType != EToolType::None; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user