Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 988c8305bc | |||
| 5ee52ec752 | |||
| 3ece1d76de | |||
| 3222d2ed3d | |||
| 41c38a35b2 | |||
| 6399a2f89f | |||
| f5d3d080c8 | |||
| cac66ebaee | |||
| c155d847e9 | |||
| f567540f22 | |||
| 688302d985 | |||
| f9d7c3f00e | |||
| 28c6ef199f | |||
| ff79448ce8 |
@@ -1,58 +1,59 @@
|
|||||||
name: 🔁 Branch Promotions
|
name: 🔁 Weekly Branch Promotions
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 6 * * 1,4" # Lundi et Jeudi à 6h UTC (design → develop)
|
- cron: "0 6 * * 1"
|
||||||
- cron: "0 6 * * 1" # Lundi à 6h UTC (develop → main)
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
promotion:
|
|
||||||
description: "Which promotion to run"
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- design-to-develop
|
|
||||||
- develop-to-main
|
|
||||||
- both
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: branch-promotions
|
group: weekly-branch-promotions
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
design-to-develop:
|
open-promotion-pr:
|
||||||
name: Open design → develop
|
name: Open ${{ matrix.head }} → ${{ matrix.base }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
strategy:
|
||||||
(github.event_name == 'schedule') ||
|
fail-fast: false
|
||||||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.promotion == 'design-to-develop' || github.event.inputs.promotion == 'both'))
|
matrix:
|
||||||
|
include:
|
||||||
|
- head: develop
|
||||||
|
base: design
|
||||||
|
title: "chore: merge develop into design"
|
||||||
|
- head: design
|
||||||
|
base: main
|
||||||
|
title: "chore: merge design into main"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ⬇️ Checkout
|
- name: ⬇️ Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 🔁 Open promotion PR
|
- name: 🔁 Open promotion PR
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
BASE_BRANCH: ${{ matrix.base }}
|
||||||
|
HEAD_BRANCH: ${{ matrix.head }}
|
||||||
|
PR_TITLE: ${{ matrix.title }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
git fetch origin develop design
|
git fetch origin "$BASE_BRANCH" "$HEAD_BRANCH"
|
||||||
|
|
||||||
if git merge-base --is-ancestor origin/design origin/develop; then
|
if git merge-base --is-ancestor "origin/$HEAD_BRANCH" "origin/$BASE_BRANCH"; then
|
||||||
echo "No promotion needed: develop already contains design."
|
echo "No promotion needed: $BASE_BRANCH already contains $HEAD_BRANCH."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
existing_pr="$(gh pr list \
|
existing_pr="$(gh pr list \
|
||||||
--state open \
|
--state open \
|
||||||
--base develop \
|
--base "$BASE_BRANCH" \
|
||||||
--head "$GITHUB_REPOSITORY_OWNER:design" \
|
--head "$GITHUB_REPOSITORY_OWNER:$HEAD_BRANCH" \
|
||||||
--json number \
|
--json number \
|
||||||
--jq '.[0].number // empty')"
|
--jq '.[0].number // empty')"
|
||||||
|
|
||||||
@@ -62,50 +63,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
gh pr create \
|
gh pr create \
|
||||||
--base develop \
|
--base "$BASE_BRANCH" \
|
||||||
--head design \
|
--head "$HEAD_BRANCH" \
|
||||||
--title "chore: merge design into develop" \
|
--title "$PR_TITLE" \
|
||||||
--body "Automated promotion PR from \`design\` to \`develop\`."
|
--body "Automated weekly promotion PR from \`$HEAD_BRANCH\` to \`$BASE_BRANCH\`."
|
||||||
|
|
||||||
develop-to-main:
|
|
||||||
name: Open develop → main
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: |
|
|
||||||
(github.event_name == 'schedule' && github.event.schedule == '0 6 * * 1') ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.promotion == 'develop-to-main' || github.event.inputs.promotion == 'both'))
|
|
||||||
steps:
|
|
||||||
- name: ⬇️ Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: 🔁 Open promotion PR
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
git fetch origin main develop
|
|
||||||
|
|
||||||
if git merge-base --is-ancestor origin/develop origin/main; then
|
|
||||||
echo "No promotion needed: main already contains develop."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
existing_pr="$(gh pr list \
|
|
||||||
--state open \
|
|
||||||
--base main \
|
|
||||||
--head "$GITHUB_REPOSITORY_OWNER:develop" \
|
|
||||||
--json number \
|
|
||||||
--jq '.[0].number // empty')"
|
|
||||||
|
|
||||||
if [ -n "$existing_pr" ]; then
|
|
||||||
echo "Promotion PR already open: #$existing_pr."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh pr create \
|
|
||||||
--base main \
|
|
||||||
--head develop \
|
|
||||||
--title "chore: merge develop into main" \
|
|
||||||
--body "Automated weekly promotion PR from \`develop\` to \`main\`."
|
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# Game States & Substates
|
||||||
|
|
||||||
|
Documentation technique pour le testing et debugging du flow de jeu.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
```
|
||||||
|
intro ──► bike ──► pylone ──► ferme ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main States
|
||||||
|
|
||||||
|
| State | Description |
|
||||||
|
| -------- | ------------------------------------------------------------------ |
|
||||||
|
| `intro` | Séquence d'introduction (cinématique, naming, premier déplacement) |
|
||||||
|
| `bike` | Mission de réparation du vélo |
|
||||||
|
| `pylone` | Quête du pylone (alert → searching → helped → manipulation) |
|
||||||
|
| `ferme` | Mission de réparation de la ferme |
|
||||||
|
| `outro` | Fin du jeu |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Intro (GameStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ intro.currentStep │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
intro ──► sequence_video ──► naming ──► start-move ──► bike
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| ---------------- | -------- | ---------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| `intro` | Initial | État initial | Auto → `sequence_video` quand `sceneReady: true` |
|
||||||
|
| `sequence_video` | GameFlow | Déclenche la cinématique dans `GameCinematics` | Fin cinématique → `naming` |
|
||||||
|
| `naming` | IntroUI | Affiche l'input pour le prénom | Submit → `start-move` |
|
||||||
|
| `start-move` | GameFlow | Active le mouvement (`canMove: true`) | Via zone "fabrikExit" → `bike` |
|
||||||
|
|
||||||
|
### Transition vers bike
|
||||||
|
|
||||||
|
- **Trigger**: Zone `fabrikExit` dans `zones.ts`
|
||||||
|
- **Action**: `advanceGameState()` dans `ZoneDetection.tsx`
|
||||||
|
- **Résultat**: `mainState: "bike"`, `intro.hasCompleted: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bike (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ bike.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers pylone
|
||||||
|
|
||||||
|
- **Trigger**: `bike.currentStep: "done"`
|
||||||
|
- **Action**: `completeBikeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "pylone"`, `pylone.currentStep: "locked"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pylone (PyloneStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ pylone.currentStep (PyloneStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked (bypass) ──► alert ──► searching ──► helped ──► manipulation ──► outro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étapes
|
||||||
|
|
||||||
|
| Step | Trigger | Action | Passage vers |
|
||||||
|
| -------------- | ---------------------------------- | ---------------------------------- | --------------------------------------------------------- |
|
||||||
|
| `locked` | Initial (après bike) | État initial | **Bypass automatique** → `alert` via `advanceGameState()` |
|
||||||
|
| `alert` | `advanceGameState()` | Affiche l'alerte (à implémenter) | Via `advanceGameState()` |
|
||||||
|
| `searching` | `advanceGameState()` | Déclenché par zone "searchingZone" | Via `advanceGameState()` |
|
||||||
|
| `helped` | Interaction avec `NPCHelper` | Dialogue avec le villageois | Via interaction 3D |
|
||||||
|
| `manipulation` | Interaction avec `PyloneDestroyed` | Interaction avec l'objet central | Via `advanceGameState()` → `outro` |
|
||||||
|
|
||||||
|
### Bypass automatique
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// useGameStore.ts - advancePyloneStep()
|
||||||
|
if (state.pylone.currentStep === "locked") {
|
||||||
|
return { pylone: { ...state.pylone, currentStep: "alert" } };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `pylone.currentStep: "manipulation"` + `advanceGameState()`
|
||||||
|
- **Action**: `advancePyloneStep()` détecte fin de la séquence
|
||||||
|
- **Résultat**: `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ferme (MissionStep)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ ferme.currentStep (MissionStep) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
locked ──► waiting ──► inspected ──► fragmented ──► scanning ──► repairing ──► reassembling ──► done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition vers outro
|
||||||
|
|
||||||
|
- **Trigger**: `ferme.currentStep: "done"`
|
||||||
|
- **Action**: `completeFermeState()` dans useGameStore
|
||||||
|
- **Résultat**: `mainState: "outro"`, `ferme.irrigationFixed: true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Outro
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ outro.hasStarted │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
waiting ──► started
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Panel
|
||||||
|
|
||||||
|
Le debug panel permet de tester toutes les transitions :
|
||||||
|
|
||||||
|
### Utilisation
|
||||||
|
|
||||||
|
1. Ouvrir le jeu en mode debug (`Debug: true` dans `Debug.ts`)
|
||||||
|
2. Le panneau "Game State" apparaît en bas à gauche
|
||||||
|
3. **Main state**: Sélectionner le state principal
|
||||||
|
4. **Sub state**: Sélectionner le sub-state
|
||||||
|
5. **Previous/Next step**: Avancer ou reculer d'un step
|
||||||
|
6. **Reset**: Remettre à l'état initial
|
||||||
|
|
||||||
|
### Raccourcis clavier
|
||||||
|
|
||||||
|
| Action | Clavier |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Avancer | Debug panel button |
|
||||||
|
| Reculer | Debug panel button |
|
||||||
|
| Reset | Debug panel button |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment tester chaque section
|
||||||
|
|
||||||
|
### Tester l'intro
|
||||||
|
|
||||||
|
1. Vérifier que `sceneReady: false` au démarrage
|
||||||
|
2. Attendre que le loader termine (`sceneReady: true`)
|
||||||
|
3. Vérifier `intro.currentStep: "intro"` → auto vers `sequence_video`
|
||||||
|
4. Si cinématique fonctionne : `sequence_video` → `naming`
|
||||||
|
5. Entrer un prénom : `naming` → `start-move`
|
||||||
|
6. Vérifier `canMove: true` après `start-move`
|
||||||
|
7. Entrer dans la zone `fabrikExit` → `mainState: "bike"`
|
||||||
|
|
||||||
|
### Tester bike
|
||||||
|
|
||||||
|
1. Via debug panel, avancer jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"`
|
||||||
|
|
||||||
|
### Tester pylone
|
||||||
|
|
||||||
|
1. Via debug panel, avancer (bypass `locked` → `alert`)
|
||||||
|
2. Vérifier `pylone.currentStep: "alert"`
|
||||||
|
3. Avancer : `alert` → `searching` → `helped` → `manipulation`
|
||||||
|
4. Après `manipulation`, vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
### Tester ferme
|
||||||
|
|
||||||
|
1. Via debug panel, avancer dans bike jusqu'à `done`
|
||||||
|
2. Vérifier `mainState: "pylone"` → `ferme` (après pylone)
|
||||||
|
3. Avancer jusqu'à `done`
|
||||||
|
4. Vérifier `mainState: "outro"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
| ------------------------------------------------- | ------------------------------------- |
|
||||||
|
| `src/managers/stores/useGameStore.ts` | Store Zustand avec toutes les actions |
|
||||||
|
| `src/types/game.ts` | Définition de `GameStep` |
|
||||||
|
| `src/types/gameplay/pylone.ts` | Définition de `PyloneStep` |
|
||||||
|
| `src/types/gameplay/repairMission.ts` | Définition de `MissionStep` |
|
||||||
|
| `src/components/game/GameFlow.tsx` | Logique de transition de l'intro |
|
||||||
|
| `src/components/zone/ZoneDetection.tsx` | Déclenchement des zones |
|
||||||
|
| `src/components/ui/debug/GameStateDebugPanel.tsx` | Outil de debug |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## État initial
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
mainState: "intro",
|
||||||
|
isCinematicPlaying: false,
|
||||||
|
sceneReady: false,
|
||||||
|
missionFlow: {
|
||||||
|
activityCity: true,
|
||||||
|
canMove: false,
|
||||||
|
dialogMessage: null,
|
||||||
|
playerName: "",
|
||||||
|
},
|
||||||
|
intro: { currentStep: "intro", dialogueAudio: null, hasCompleted: false, isBikeUnlocked: false },
|
||||||
|
bike: { currentStep: "locked", dialogueAudio: null, isRepaired: false },
|
||||||
|
pylone: { currentStep: "locked", dialogueAudio: null, isPowered: false },
|
||||||
|
ferme: { currentStep: "locked", dialogueAudio: null, irrigationFixed: false },
|
||||||
|
outro: { dialogueAudio: null, hasStarted: false },
|
||||||
|
}
|
||||||
|
```
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,21 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"cinematics": [
|
"cinematics": [
|
||||||
|
{
|
||||||
|
"id": "intro_sequence",
|
||||||
|
"trigger": "intro_sequence",
|
||||||
|
"cameraKeyframes": [
|
||||||
|
{ "time": 0, "position": [8, 5, 12], "target": [0, 2, 0] },
|
||||||
|
{ "time": 8, "position": [12, 4, -6], "target": [10, 1.4, -8] },
|
||||||
|
{ "time": 16, "position": [5, 6, -15], "target": [0, 3, -20] },
|
||||||
|
{ "time": 24, "position": [0, 8, -30], "target": [0, 0, -40] }
|
||||||
|
],
|
||||||
|
"dialogueCues": [
|
||||||
|
{ "time": 0, "dialogueId": "intro_welcome" },
|
||||||
|
{ "time": 8, "dialogueId": "intro_explanation" },
|
||||||
|
{ "time": 16, "dialogueId": "intro_mission" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "intro_overview",
|
"id": "intro_overview",
|
||||||
"timecode": 0,
|
"timecode": 0,
|
||||||
|
|||||||
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.
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.
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.
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.
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.
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,64 +1,36 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { AudioManager } from "@/managers/AudioManager";
|
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
import { AUDIO_PATHS } from "@/data/audioConfig";
|
|
||||||
|
|
||||||
export function GameFlow(): null {
|
export function GameFlow(): null {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setStep = useGameStore((state) => state.setIntroStep);
|
||||||
const setActivityCity = useGameStore((state) => state.setActivityCity);
|
const playVideo = useGameStore((state) => state.playVideo);
|
||||||
|
const isCinematicPlaying = useGameStore((state) => state.isCinematicPlaying);
|
||||||
|
const sceneReady = useGameStore((state) => state.sceneReady);
|
||||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
const hasInitialized = useRef(false);
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasInitialized.current && step === "intro") {
|
if (!hasInitialized.current && step === "intro" && sceneReady) {
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
setStep("start-intro");
|
setStep("sequence_video");
|
||||||
|
playVideo("/videos/intro.webm");
|
||||||
}
|
}
|
||||||
}, [step, setStep]);
|
}, [step, setStep, sceneReady, playVideo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step === "start-intro") {
|
if (step === "sequence_video" && !isCinematicPlaying) {
|
||||||
const audio = AudioManager.getInstance();
|
setStep("start-move");
|
||||||
audio.playSoundWithCallback(AUDIO_PATHS.intro, 0.5, () => {
|
|
||||||
setStep("naming");
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {};
|
|
||||||
}
|
}
|
||||||
|
}, [step, isCinematicPlaying, setStep]);
|
||||||
|
|
||||||
if (step === "bienvenue") {
|
useEffect(() => {
|
||||||
const audio = AudioManager.getInstance();
|
if (step === "start-move") {
|
||||||
audio.playSoundWithCallback(AUDIO_PATHS.bienvenue, 0.5, () => {
|
|
||||||
setCanMove(true);
|
setCanMove(true);
|
||||||
setStep("star-move");
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "mission2") {
|
|
||||||
setActivityCity(false);
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.alertCentral, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "searching") {
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.searching, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "helped") {
|
|
||||||
const audio = AudioManager.getInstance();
|
|
||||||
audio.playSound(AUDIO_PATHS.helped, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (step === "manipulation") {
|
|
||||||
setCanMove(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [step, setStep, setActivityCity, setCanMove]);
|
}, [step, setCanMove]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { createNetShader } from "@/shaders/NetShader";
|
||||||
|
|
||||||
|
export function NetTest(): React.JSX.Element {
|
||||||
|
const materialRef = useRef<THREE.ShaderMaterial>(null);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
if (materialRef.current) {
|
||||||
|
materialRef.current.uniforms.uTime.value += delta;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mesh position={[0, 2, -5]}>
|
||||||
|
<planeGeometry args={[2, 2, 1, 1]} />
|
||||||
|
<primitive
|
||||||
|
object={createNetShader()}
|
||||||
|
ref={materialRef}
|
||||||
|
attach="material"
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,17 +8,17 @@ interface NPCHelperProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element {
|
export function NPCHelper({ position }: NPCHelperProps): React.JSX.Element {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.pylone.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
|
|
||||||
const handlePress = (): void => {
|
const handlePress = (): void => {
|
||||||
if (step === "searching") {
|
if (step === "searching") {
|
||||||
setStep("helped");
|
setPyloneStep({ currentStep: "helped" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShow = step === "searching" || debug.active;
|
const shouldShow = step === "searching" || step === "helped" || debug.active;
|
||||||
|
|
||||||
if (!shouldShow) {
|
if (!shouldShow) {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ interface PyloneDestroyedProps {
|
|||||||
export function PyloneDestroyed({
|
export function PyloneDestroyed({
|
||||||
position,
|
position,
|
||||||
}: PyloneDestroyedProps): React.JSX.Element {
|
}: PyloneDestroyedProps): React.JSX.Element {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.pylone.currentStep);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
const setCanMove = useGameStore((state) => state.setCanMove);
|
const setCanMove = useGameStore((state) => state.setCanMove);
|
||||||
const showDialog = useGameStore((state) => state.showDialog);
|
const showDialog = useGameStore((state) => state.showDialog);
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
@@ -19,7 +19,7 @@ export function PyloneDestroyed({
|
|||||||
const handlePress = (): void => {
|
const handlePress = (): void => {
|
||||||
if (step === "helped") {
|
if (step === "helped") {
|
||||||
setCanMove(false);
|
setCanMove(false);
|
||||||
setStep("manipulation");
|
setPyloneStep({ currentStep: "manipulation" });
|
||||||
} else if (step === "searching") {
|
} else if (step === "searching") {
|
||||||
showDialog(
|
showDialog(
|
||||||
"Cet objet est trop lourd pour le porter tout seul, trouve de l'aide",
|
"Cet objet est trop lourd pour le porter tout seul, trouve de l'aide",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
export interface SimpleModelConfig extends ModelTransformProps {
|
export interface SimpleModelConfig extends ModelTransformProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
@@ -30,12 +29,6 @@ export function SimpleModel({
|
|||||||
});
|
});
|
||||||
const model = useMemo(() => scene.clone(true), [scene]);
|
const model = useMemo(() => scene.clone(true), [scene]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(model);
|
|
||||||
};
|
|
||||||
}, [model]);
|
|
||||||
|
|
||||||
const parsedScale =
|
const parsedScale =
|
||||||
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
typeof scale === "number" ? ([scale, scale, scale] as Vector3Tuple) : scale;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { Component, useEffect, useMemo, useRef, type ReactNode } from "react";
|
import { Component, useMemo, useRef, type ReactNode } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
interface SkyModelProps {
|
interface SkyModelProps {
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
@@ -81,12 +80,6 @@ function SkyModelContent({
|
|||||||
});
|
});
|
||||||
const model = useMemo(() => createSkyModel(scene), [scene]);
|
const model = useMemo(() => createSkyModel(scene), [scene]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(model);
|
|
||||||
};
|
|
||||||
}, [model]);
|
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(() => {
|
||||||
groupRef.current?.position.copy(camera.position);
|
groupRef.current?.position.copy(camera.position);
|
||||||
});
|
});
|
||||||
@@ -129,5 +122,5 @@ function createSkyMaterial<T extends THREE.Material>(material: T): T {
|
|||||||
return skyMaterial as T;
|
return skyMaterial as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
useGLTF.preload("/models/skybox/model.gltf");
|
useGLTF.preload("/models/skybox/skybox.gltf");
|
||||||
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
useGLTF.preload(LEGACY_SKY_MODEL_PATH);
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { useEffect, useMemo, useRef } from "react";
|
|
||||||
import * as THREE from "three";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
const TERRAIN_MODEL_PATH = "/models/terrain/model.gltf";
|
|
||||||
const TERRAIN_DEFAULT_POSITION: Vector3Tuple = [0, 0, 0];
|
|
||||||
|
|
||||||
interface TerrainModelProps {
|
|
||||||
position?: Vector3Tuple;
|
|
||||||
rotation?: Vector3Tuple;
|
|
||||||
scale?: Vector3Tuple;
|
|
||||||
receiveShadow?: boolean;
|
|
||||||
visible?: boolean;
|
|
||||||
groupRef?: React.RefObject<THREE.Group>;
|
|
||||||
onLoaded?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTerrainMaterialSettings(
|
|
||||||
scene: THREE.Object3D,
|
|
||||||
receiveShadow: boolean,
|
|
||||||
): void {
|
|
||||||
scene.traverse((child) => {
|
|
||||||
if (child instanceof THREE.Mesh) {
|
|
||||||
child.receiveShadow = receiveShadow;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TerrainModel({
|
|
||||||
position = TERRAIN_DEFAULT_POSITION,
|
|
||||||
rotation = [0, 0, 0],
|
|
||||||
scale = [1, 1, 1],
|
|
||||||
receiveShadow = true,
|
|
||||||
visible = true,
|
|
||||||
groupRef,
|
|
||||||
onLoaded,
|
|
||||||
}: TerrainModelProps): React.JSX.Element {
|
|
||||||
const internalRef = useRef<THREE.Group>(null);
|
|
||||||
const ref = groupRef ?? internalRef;
|
|
||||||
const { scene } = useGLTF(TERRAIN_MODEL_PATH);
|
|
||||||
|
|
||||||
const terrainModel = useMemo(() => {
|
|
||||||
const model = scene.clone(true);
|
|
||||||
applyTerrainMaterialSettings(model, receiveShadow);
|
|
||||||
return model;
|
|
||||||
}, [scene, receiveShadow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(terrainModel);
|
|
||||||
};
|
|
||||||
}, [terrainModel]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLoaded?.();
|
|
||||||
}, [onLoaded]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group
|
|
||||||
ref={ref}
|
|
||||||
position={position}
|
|
||||||
rotation={rotation}
|
|
||||||
scale={scale}
|
|
||||||
visible={visible}
|
|
||||||
>
|
|
||||||
<primitive object={terrainModel} />
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useGLTF.preload(TERRAIN_MODEL_PATH);
|
|
||||||
@@ -1,106 +1,10 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
export function IntroUI(): React.JSX.Element | null {
|
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
|
||||||
const setPlayerName = useGameStore((state) => state.setPlayerName);
|
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
|
||||||
const [inputValue, setInputValue] = useState("");
|
|
||||||
|
|
||||||
if (step !== "naming") return null;
|
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
|
||||||
if (inputValue.trim() === "") return;
|
|
||||||
|
|
||||||
setPlayerName(inputValue.trim());
|
|
||||||
setStep("bienvenue");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent): void => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#1a1a1a",
|
|
||||||
padding: "2rem",
|
|
||||||
borderRadius: "12px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "1.5rem",
|
|
||||||
minWidth: "300px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
style={{
|
|
||||||
color: "#fff",
|
|
||||||
margin: 0,
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Quel est votre prenom ?
|
|
||||||
</h2>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="Votre prenom"
|
|
||||||
autoFocus
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
borderRadius: "6px",
|
|
||||||
border: "1px solid #444",
|
|
||||||
backgroundColor: "#2a2a2a",
|
|
||||||
color: "#fff",
|
|
||||||
outline: "none",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={inputValue.trim() === ""}
|
|
||||||
style={{
|
|
||||||
padding: "0.75rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
borderRadius: "6px",
|
|
||||||
border: "none",
|
|
||||||
backgroundColor: inputValue.trim() ? "#4a9" : "#444",
|
|
||||||
color: "#fff",
|
|
||||||
cursor: inputValue.trim() ? "pointer" : "not-allowed",
|
|
||||||
transition: "background-color 0.2s",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Valider
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BienvenueDisplay(): React.JSX.Element | null {
|
export function BienvenueDisplay(): React.JSX.Element | null {
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
const playerName = useGameStore((state) => state.missionFlow.playerName);
|
const playerName = useGameStore((state) => state.missionFlow.playerName);
|
||||||
|
|
||||||
if (step !== "bienvenue") return null;
|
if (step !== "start-move") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||||
|
|
||||||
|
export function VideoPlayer(): null {
|
||||||
|
const currentVideo = useGameStore((state) => state.missionFlow.currentVideo);
|
||||||
|
const clearVideo = useGameStore((state) => state.clearVideo);
|
||||||
|
const setCinematicPlaying = useGameStore(
|
||||||
|
(state) => state.setCinematicPlaying,
|
||||||
|
);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentVideo) {
|
||||||
|
setCinematicPlaying(true);
|
||||||
|
}
|
||||||
|
}, [currentVideo, setCinematicPlaying]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && currentVideo) {
|
||||||
|
closeVideo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [currentVideo]);
|
||||||
|
|
||||||
|
const closeVideo = () => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.pause();
|
||||||
|
}
|
||||||
|
clearVideo();
|
||||||
|
setCinematicPlaying(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
closeVideo();
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener("ended", handleEnded);
|
||||||
|
return () => video.removeEventListener("ended", handleEnded);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!currentVideo) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.95)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={currentVideo}
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={closeVideo}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/managers/stores/useGameStore";
|
} from "@/managers/stores/useGameStore";
|
||||||
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
|
import { isMissionStep, MISSION_STEPS } from "@/types/gameplay/repairMission";
|
||||||
import { GAME_STEPS, type GameStep } from "@/types/game";
|
import { GAME_STEPS, type GameStep } from "@/types/game";
|
||||||
|
import { PYLONE_STEPS, type PyloneStep } from "@/types/gameplay/pylone";
|
||||||
|
|
||||||
const MAIN_STATES: MainGameState[] = [
|
const MAIN_STATES: MainGameState[] = [
|
||||||
"intro",
|
"intro",
|
||||||
@@ -54,6 +55,8 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
const subStateOptions =
|
const subStateOptions =
|
||||||
mainState === "intro"
|
mainState === "intro"
|
||||||
? GAME_STEPS
|
? GAME_STEPS
|
||||||
|
: mainState === "pylone"
|
||||||
|
? PYLONE_STEPS
|
||||||
: mainState === "outro"
|
: mainState === "outro"
|
||||||
? ["waiting", "started"]
|
? ["waiting", "started"]
|
||||||
: MISSION_STEPS;
|
: MISSION_STEPS;
|
||||||
@@ -64,6 +67,11 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mainState === "pylone") {
|
||||||
|
setPyloneState({ currentStep: nextSubState as PyloneStep });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mainState === "outro") {
|
if (mainState === "outro") {
|
||||||
setOutroState({ hasStarted: nextSubState === "started" });
|
setOutroState({ hasStarted: nextSubState === "started" });
|
||||||
return;
|
return;
|
||||||
@@ -76,11 +84,6 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainState === "pylone") {
|
|
||||||
setPyloneState({ currentStep: nextSubState });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainState === "ferme") {
|
if (mainState === "ferme") {
|
||||||
setFermeState({ currentStep: nextSubState });
|
setFermeState({ currentStep: nextSubState });
|
||||||
return;
|
return;
|
||||||
@@ -95,11 +98,6 @@ export function GameStateDebugPanel(): React.JSX.Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextMainState === "pylone" && pyloneStep === "locked") {
|
|
||||||
setPyloneState({ currentStep: "waiting" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextMainState === "ferme" && fermeStep === "locked") {
|
if (nextMainState === "ferme" && fermeStep === "locked") {
|
||||||
setFermeState({ currentStep: "waiting" });
|
setFermeState({ currentStep: "waiting" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ export function ZoneDetection(): null {
|
|||||||
const triggeredZones = useRef<Set<string>>(new Set());
|
const triggeredZones = useRef<Set<string>>(new Set());
|
||||||
const debug = Debug.getInstance();
|
const debug = Debug.getInstance();
|
||||||
const step = useGameStore((state) => state.intro.currentStep);
|
const step = useGameStore((state) => state.intro.currentStep);
|
||||||
|
const mainState = useGameStore((state) => state.mainState);
|
||||||
const setStep = useGameStore((state) => state.setIntroStep);
|
const setStep = useGameStore((state) => state.setIntroStep);
|
||||||
|
const setPyloneStep = useGameStore((state) => state.setPyloneState);
|
||||||
|
const advanceGameState = useGameStore((state) => state.advanceGameState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!debug.active) return;
|
if (!debug.active) return;
|
||||||
@@ -65,7 +68,11 @@ export function ZoneDetection(): null {
|
|||||||
const distanceSq = _playerPos.distanceToSquared(_zonePos);
|
const distanceSq = _playerPos.distanceToSquared(_zonePos);
|
||||||
|
|
||||||
if (distanceSq <= zone.radius * zone.radius) {
|
if (distanceSq <= zone.radius * zone.radius) {
|
||||||
|
if (zone.targetStep === "bike" && mainState === "intro") {
|
||||||
|
advanceGameState();
|
||||||
|
} else {
|
||||||
setStep(zone.targetStep);
|
setStep(zone.targetStep);
|
||||||
|
}
|
||||||
triggeredZones.current.add(zone.id);
|
triggeredZones.current.add(zone.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,51 +85,6 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
pylone: {
|
|
||||||
id: "pylone",
|
|
||||||
label: "Power pylon",
|
|
||||||
description:
|
|
||||||
"Restore the pylon lamp relay and damaged panel before reconnecting the grid",
|
|
||||||
modelPath: "/models/pylone/model.gltf",
|
|
||||||
stageUiPath: "/assets/UI/centrale.webm",
|
|
||||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
|
||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
|
||||||
case: DEFAULT_REPAIR_CASE,
|
|
||||||
reassemblySeconds: 1.8,
|
|
||||||
requiredReplacementPartId: "pylone-grid-relay-replacement",
|
|
||||||
scanPartSeconds: 1.4,
|
|
||||||
brokenParts: [
|
|
||||||
{
|
|
||||||
id: "pylone-grid-relay",
|
|
||||||
label: "Grid relay",
|
|
||||||
nodeName: "lampe",
|
|
||||||
placeholderName: "placeholder_1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-damaged-panel",
|
|
||||||
label: "Damaged solar panel",
|
|
||||||
nodeName: "panneau2",
|
|
||||||
placeholderName: "placeholder_2",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
replacementParts: [
|
|
||||||
{
|
|
||||||
id: "pylone-grid-relay-replacement",
|
|
||||||
label: "Replacement grid relay",
|
|
||||||
modelPath: "/models/pylone/model.gltf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-stone-decoy",
|
|
||||||
label: "Stone counterweight",
|
|
||||||
modelPath: "/models/galet/model.gltf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "pylone-cooling-decoy",
|
|
||||||
label: "Cooling core",
|
|
||||||
modelPath: "/models/refroidisseur/model.gltf",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
ferme: {
|
ferme: {
|
||||||
id: "ferme",
|
id: "ferme",
|
||||||
label: "Vertical farm",
|
label: "Vertical farm",
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export const PLAYER_MAX_DELTA = 0.05;
|
|||||||
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
export const PLAYER_ACCELERATION_MULTIPLIER = 9;
|
||||||
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
export const PLAYER_XZ_DAMPING_FACTOR = 8;
|
||||||
|
|
||||||
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [0, 50, 0];
|
export const PLAYER_SPAWN_POSITION_GAME: Vector3Tuple = [6.56,5,71.55];
|
||||||
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
export const PLAYER_SPAWN_POSITION_PHYSICS: Vector3Tuple = [0, 3, 0];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/skybox.gltf";
|
export const GAME_SCENE_SKY_MODEL_PATH = "/models/skybox/skybox.gltf";
|
||||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
export const GAME_SCENE_FALLBACK_SKY_MODEL_PATH = "/models/sky/model.glb";
|
||||||
export const GAME_SCENE_SKY_MODEL_SCALE = 100;
|
export const GAME_SCENE_SKY_MODEL_SCALE = 300;
|
||||||
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
export const GAME_SCENE_FALLBACK_SKY_MODEL_SCALE = 1;
|
||||||
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
export const PHYSICS_SCENE_BACKGROUND_COLOR = "#0b1018";
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { TERRAIN_COLORS } from "@/data/world/terrainConfig";
|
|
||||||
|
|
||||||
export const FOG_CONFIG = {
|
|
||||||
enabled: true,
|
|
||||||
color: "#c8dbbe",
|
|
||||||
near: 50,
|
|
||||||
far: 70,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CHUNK_CONFIG = {
|
|
||||||
enabled: true,
|
|
||||||
chunkSize: 40,
|
|
||||||
loadRadius: 70,
|
|
||||||
unloadRadius: 80,
|
|
||||||
updateInterval: 500,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GROUND_PLANE_COLOR = TERRAIN_COLORS.grass1.hex;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export const GRAPHICS_DEFAULTS = {
|
|
||||||
dynamicGrass: true,
|
|
||||||
dynamicTrees: true,
|
|
||||||
dynamicClouds: true,
|
|
||||||
shadowsEnabled: true,
|
|
||||||
grassDensity: 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GRAPHICS_BOUNDS = {
|
|
||||||
grassDensity: { min: 0.1, max: 2.0, step: 0.1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GraphicsState = typeof GRAPHICS_DEFAULTS;
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
export const TERRAIN_COLORS = {
|
|
||||||
grass1: {
|
|
||||||
hex: "#84C66B",
|
|
||||||
rgb: [132, 198, 107] as const,
|
|
||||||
type: "grass" as const,
|
|
||||||
grassTipColor: "#84C66B",
|
|
||||||
},
|
|
||||||
grass2: {
|
|
||||||
hex: "#67B058",
|
|
||||||
rgb: [103, 176, 88] as const,
|
|
||||||
type: "grass" as const,
|
|
||||||
grassTipColor: "#67B058",
|
|
||||||
},
|
|
||||||
grass3: {
|
|
||||||
hex: "#A3CA5B",
|
|
||||||
rgb: [163, 202, 91] as const,
|
|
||||||
type: "grass" as const,
|
|
||||||
grassTipColor: "#A3CA5B",
|
|
||||||
},
|
|
||||||
potager: {
|
|
||||||
hex: "#342420",
|
|
||||||
rgb: [52, 36, 32] as const,
|
|
||||||
type: "tile" as const,
|
|
||||||
tileModel: "/models/potager/potager.gltf",
|
|
||||||
tileSize: 1,
|
|
||||||
},
|
|
||||||
terre: {
|
|
||||||
hex: "#513E2C",
|
|
||||||
rgb: [81, 62, 44] as const,
|
|
||||||
type: "none" as const,
|
|
||||||
},
|
|
||||||
chemin: {
|
|
||||||
hex: "#F5D896",
|
|
||||||
rgb: [245, 216, 150] as const,
|
|
||||||
type: "tile" as const,
|
|
||||||
tileModel: "/models/chemins/model.gltf",
|
|
||||||
tileSize: 1,
|
|
||||||
},
|
|
||||||
eau: {
|
|
||||||
hex: "#91DAF5",
|
|
||||||
rgb: [145, 218, 245] as const,
|
|
||||||
type: "water" as const,
|
|
||||||
},
|
|
||||||
cailloux: {
|
|
||||||
hex: "#B6D3DE",
|
|
||||||
rgb: [182, 211, 222] as const,
|
|
||||||
type: "none" as const,
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type TerrainColorKey = keyof typeof TERRAIN_COLORS;
|
|
||||||
export type TerrainType = "grass" | "tile" | "water" | "none";
|
|
||||||
|
|
||||||
export const GRASS_BASE_COLOR = "#1a3a1a";
|
|
||||||
|
|
||||||
export const COLOR_TOLERANCE = 15;
|
|
||||||
|
|
||||||
export function colorMatchesTerrain(
|
|
||||||
r: number,
|
|
||||||
g: number,
|
|
||||||
b: number,
|
|
||||||
targetRgb: readonly [number, number, number],
|
|
||||||
tolerance: number = COLOR_TOLERANCE,
|
|
||||||
): boolean {
|
|
||||||
return (
|
|
||||||
Math.abs(r - targetRgb[0]) <= tolerance &&
|
|
||||||
Math.abs(g - targetRgb[1]) <= tolerance &&
|
|
||||||
Math.abs(b - targetRgb[2]) <= tolerance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTerrainTypeFromColor(
|
|
||||||
r: number,
|
|
||||||
g: number,
|
|
||||||
b: number,
|
|
||||||
): TerrainColorKey | null {
|
|
||||||
for (const [key, config] of Object.entries(TERRAIN_COLORS)) {
|
|
||||||
if (colorMatchesTerrain(r, g, b, config.rgb)) {
|
|
||||||
return key as TerrainColorKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isGrassZone(r: number, g: number, b: number): boolean {
|
|
||||||
return (
|
|
||||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass1.rgb) ||
|
|
||||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass2.rgb) ||
|
|
||||||
colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass3.rgb)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGrassTipColor(
|
|
||||||
r: number,
|
|
||||||
g: number,
|
|
||||||
b: number,
|
|
||||||
): string | null {
|
|
||||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass1.rgb)) {
|
|
||||||
return TERRAIN_COLORS.grass1.grassTipColor;
|
|
||||||
}
|
|
||||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass2.rgb)) {
|
|
||||||
return TERRAIN_COLORS.grass2.grassTipColor;
|
|
||||||
}
|
|
||||||
if (colorMatchesTerrain(r, g, b, TERRAIN_COLORS.grass3.rgb)) {
|
|
||||||
return TERRAIN_COLORS.grass3.grassTipColor;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
export const WIND_DEFAULTS = {
|
|
||||||
speed: 0.3,
|
|
||||||
direction: Math.PI * 0.25,
|
|
||||||
strength: 1.0,
|
|
||||||
noiseScale: 0.9,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WIND_BOUNDS = {
|
|
||||||
speed: { min: 0, max: 2, step: 0.1 },
|
|
||||||
direction: { min: -Math.PI, max: Math.PI, step: 0.1 },
|
|
||||||
strength: { min: 0, max: 3, step: 0.1 },
|
|
||||||
noiseScale: { min: 0.1, max: 5, step: 0.1 },
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WindState = typeof WIND_DEFAULTS;
|
|
||||||
+5
-12
@@ -1,19 +1,12 @@
|
|||||||
import type { Zone } from "@/types/game";
|
import type { Zone, GameStep } from "@/types/game";
|
||||||
import type { Vector3Tuple } from "@/types/three/three";
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
export const ZONES: Zone[] = [
|
export const ZONES: Zone[] = [
|
||||||
{
|
{
|
||||||
id: "fabrikExit",
|
id: "fabrikExit",
|
||||||
position: [-5, 25, -15] as Vector3Tuple,
|
position: [18.43,0,75.3] as Vector3Tuple,
|
||||||
radius: 10,
|
radius: 4,
|
||||||
height: 20,
|
height: 10,
|
||||||
targetStep: "mission2",
|
targetStep: "bike" as GameStep,
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "searchingZone",
|
|
||||||
position: [-5, 25, -30] as Vector3Tuple,
|
|
||||||
radius: 10,
|
|
||||||
height: 20,
|
|
||||||
targetStep: "searching",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { useGameStore } from "@/managers/stores/useGameStore";
|
|||||||
import type { MissionStep } from "@/types/gameplay/repairMission";
|
import type { MissionStep } from "@/types/gameplay/repairMission";
|
||||||
|
|
||||||
export function useRepairMovementLocked(): boolean {
|
export function useRepairMovementLocked(): boolean {
|
||||||
return false;
|
|
||||||
|
|
||||||
return useGameStore((state) => {
|
return useGameStore((state) => {
|
||||||
switch (state.mainState) {
|
switch (state.mainState) {
|
||||||
case "bike":
|
case "bike":
|
||||||
return isRepairMovementLocked(state.bike.currentStep);
|
return isRepairMovementLocked(state.bike.currentStep);
|
||||||
case "pylone":
|
case "pylone":
|
||||||
return isRepairMovementLocked(state.pylone.currentStep);
|
return state.pylone.currentStep === "manipulation";
|
||||||
case "ferme":
|
case "ferme":
|
||||||
return isRepairMovementLocked(state.ferme.currentStep);
|
return isRepairMovementLocked(state.ferme.currentStep);
|
||||||
case "intro":
|
case "intro":
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import * as THREE from "three";
|
import type * as THREE from "three";
|
||||||
import { disposeObject3D } from "@/utils/three/dispose";
|
|
||||||
|
|
||||||
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
export function useClonedObject<T extends THREE.Object3D>(object: T): T {
|
||||||
const clone = useMemo(() => object.clone(true) as T, [object]);
|
return useMemo(() => object.clone(true) as T, [object]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObject3D(clone);
|
|
||||||
};
|
|
||||||
}, [clone]);
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user