By Year

Architecture By Year

Rôle

By Year est le framework historique où une activité progresse année par année. À chaque année, les équipes prennent des décisions game-specific, le serveur génère les résultats, calcule les évaluations et prépare l'année suivante.

flowchart LR
ACT[ActivityByYear] --> Y[Current Year]
Y --> DEC[TeamDecisionsByYear]
DEC --> GAME[Game-specific logic]
GAME --> RES[ResultsByYear]
RES --> EVAL[TeamEvaluation / StudentEvaluation]
RES --> NEXT[Next Year]

Unity

Couche Emplacement
Modèles Assets/Src/Game/Data/ByYear/
Controllers Assets/Src/Game/Controllers/*ByYear*
UI commune Assets/Src/UI/SimByYear/
Game-specific Assets/GameSpecific/ent1|hr1|mgt1|mk1|mk2/

Serveur

Game-specific

Contrairement à Accounting, By Year n'a pas une seule logique métier partagée. Le framework fournit le cycle; les jeux fournissent les décisions et les calculs. Il faut donc éviter de mettre les règles MK1, HR1 ou ENT1 dans une page “Common”.

Ajouter un nouveau jeu By Year de A à Z

Un nouveau jeu annuel n'est pas seulement un nouveau dossier GameSpecific. Il doit être branché côté Unity, static data, serveur, génération, évaluation, résultats, messages, UI et factories.

1. GameUid et composition Unity

  1. Ajouter le GameUid dans SharedConstants.
  2. Le mapper vers SimType.ByYear.
  3. Créer Assets/GameSpecific/<gameUid>/.
  4. Créer les modèles StaticData, Team, TeamDecisions, Results et les controllers game-specific nécessaires.
  5. Brancher les classes dans Factory.cs.

2. Static data

Ajouter le jeu dans node.js/exports/exportAllData.js avec Spreadsheet/GIDs et fichiers supplémentaires. Créer Resources/data_<gameUid>, importer puis vérifier StaticDataController<Game>.

3. Serveur game-specific

Créer server_scripts/<gameUid>/ avec :

4. Brancher les factories serveur

5. Définir le cycle annuel du jeu

Implémenter au minimum startYearGameSpec(actData), getYearData(), generateEvents(), gameSpecificInitTeam(), startCash(), anyDecisionsTaken() et les helpers métier requis.

6. Génération

GenerateResults<Game>.generateCurrentYearResults(actData) doit être l'entrée unique du calcul annuel. Il doit lire les équipes/décisions, calculer les résultats, les sauvegarder et préparer l'état de l'année suivante sans dépendre de l'UI.

7. Unity UI

Créer les contrôles game-specific dans GameSpecific/<gameUid>/Src/UI, plus les prefabs/scenes/Resources. Les composants génériques année/eval/rankings restent sous Src/UI/SimByYear.

8. Textes, scénario, tutoriels, timelines

Ajouter les textes et assets propres au jeu. Si le jeu possède ses propres tutoriels/timelines, les placer dans ses dossiers GameSpecific et les brancher via les states/UI concernés.

9. Validation minimale

  1. Créer une simulation.
  2. Démarrer année 1.
  3. Prendre au moins une décision.
  4. Générer résultats.
  5. Vérifier résultats + cash + ActPerf.
  6. Passer à l'année suivante.
  7. Réouvrir une année.
  8. Tester async/bot si supporté.
  9. Tester rankings/final sequence.

Modèle de données By Year

Objets Unity

Controllers

Les classes ActivityByYearController, SimulationByYearController, TeamByYearController, TeamEvaluationByYearController et StudentEvaluationByYearController servent de couche runtime entre ces modèles et l'UI.

Documents serveur

flowchart TD
SIM[(simulation)] --> ACT[(activity)]
ACT --> TEAM[(teams)]
ACT --> GAME[(actDataGameSpec)]
TEAM --> DEC[team decisions / activities]
ACT --> RES[(results)]
TEAM --> ET[(evaluationTeam)]
SIM --> ES[(evaluation)]
TEAM --> LOG[(teamsLogbooks)]

currentYear / currentYearState

L'activité garde l'année courante et son état. Les transitions serveur utilisent notamment les états “not started”, “year started”, “generating” et “year finished”. Les contrôles Unity affichent l'écran approprié en fonction de cet état.

years[]

Chaque entrée annuelle peut porter les dates de début/fin et le flag de pratique. Le calendrier et le mode de gestion déterminent si les transitions se font manuellement ou automatiquement.

Ajouter un champ de décision ou de résultat By Year

Décision étudiante

  1. Ajouter le champ dans le modèle Unity TeamDecisions<Game> ou YearData selon son scope.
  2. Ajouter la désérialisation avec une valeur compatible avec les anciennes équipes.
  3. Ajouter le contrôle UI qui édite cette décision.
  4. Ajouter la custom command serveur ou étendre celle qui possède déjà la décision.
  5. Sauvegarder dans le bon yearData de l'équipe.
  6. Envoyer un RTT game-specific si les coéquipiers doivent voir le changement.
  7. Consommer la décision dans GenerateResults<Game>.
  8. Mettre à jour anyDecisionsTaken() si cette décision compte pour déterminer si l'équipe a commencé.
  9. Mettre à jour les bots si une équipe bot doit prendre cette décision.

Résultat annuel

  1. Ajouter le champ au modèle serveur de résultat produit par generateCurrentYearResults().
  2. Le persister dans results.
  3. Ajouter le modèle Unity Results/ResultsByYear et désérialiser.
  4. Ajouter report/tooltip/UI.
  5. Si le champ participe à ActPerf, mettre à jour Eval<Game>.computeActPerfScore().
  6. Si le champ participe au ranking, mettre à jour updateRankings<Game>().
  7. Vérifier deleteResults()/reopen year.

Où placer une donnée annuelle

Donnée Emplacement
Décision qui change chaque année TeamDecisions<Game>.yearData[year] ou structure équivalente.
État d'équipe durable entre années Team<Game> / team data persistante.
Résultat calculé d'une année results.
Configuration immuable Static data.
Donnée commune à toute l'activité actDataGameSpec.

Compatibilité anciens documents

Une nouvelle propriété de team/activity doit être tolérante à l'absence. Si sa valeur doit absolument exister sur les vieux documents, utiliser la migration correspondante dans _utils/version/.

Cycle d’une année, calendrier et jobs

startYear()

utils_by_year.startYear(actData, updateYearFinished) incrémente l'année si nécessaire, passe l'état à ACT_YEAR_STARTED, enregistre startTime, appelle startYearGameSpec(), initialise les stubs de présence puis envoie act_year_started.

stateDiagram-v2
[*] --> NotStarted
NotStarted --> YearStarted: startYear
YearStarted --> Generating: fin de période / commande
Generating --> YearFinished: génération terminée
YearFinished --> YearStarted: année suivante
YearFinished --> [*]: dernière année

nextGenerate

updateNextGenerateDate() calcule quand le prochain traitement automatique doit avoir lieu. En pratique, les années n'ont pas de génération automatique. En mode calendrier, nextGenerate pointe vers la fin de l'année active ou le début de la suivante.

Validation des dates

validateDates() corrige les années mal ordonnées ou placées dans le passé et garantit notamment un intervalle minimal de cinq minutes. Si des dates sont déplacées, le professeur peut recevoir years_date_modified.

Job planifié

gatherActsForJob() récupère les activités dont nextGenerate < now. runActJob() traite une activité active : si l'année est commencée, elle passe à generating et appelle generateResultsJob(); sinon elle peut démarrer l'année suivante.

generateResultsJob()

  1. envoie act_generating;
  2. fait jouer les bots si l'activité est async;
  3. génère les events game-specific;
  4. appelle generateResultsByGameUid[gameUid].generateCurrentYearResults(actData);
  5. décide s'il faut démarrer automatiquement l'année suivante;
  6. met à jour nextGenerate;
  7. sauvegarde l'activité;
  8. envoie act_year_finished lorsque requis.

Scripts de job

Les wrappers se trouvent sous server_scripts/_job/, notamment generate_results_job.js et le scheduler commun.

Modifier le cycle annuel ou le calendrier sans casser les jobs

Invariants du cycle

Changer la logique de dates

Les fonctions à auditer sont updateNextGenerateDate(), validateDates(), dateToUpper(), dateToLower() et gatherActsForJob().

  1. Définir le comportement attendu pour sync, async et practice.
  2. Vérifier que les dates passées sont corrigées de façon déterministe.
  3. Vérifier l'intervalle minimal attendu par validateDates().
  4. Mettre à jour les messages professeur si des dates sont déplacées.
  5. Tester le job avec une activité dont nextGenerate est juste avant/après “now”.

Modifier startYear()

Toute nouvelle initialisation annuelle doit être faite soit dans le socle commun si elle concerne tous les jeux, soit dans startYearGameSpec(). Ne pas ajouter des branches GameUid directement dans utils_by_year.startYear().

Tester un changement de cycle

Cas À vérifier
Année 1 manuelle start state, presence, year data, RTT.
Fin manuelle generating → results → finished.
Calendrier automatique nextGenerate, scheduler, next year.
Async bots avant génération.
Dernière année pas de démarrage fantôme d'une année suivante.
Reopen état + results + evals + logbook restaurés/cohérents.

Debug d'un job qui ne part pas

  1. Lire activity.nextGenerate.
  2. Vérifier que l'activité est active.
  3. Vérifier gatherActsForJob().
  4. Exécuter runActJob() avec l'UID.
  5. Inspecter l'état annuel avant/après.
  6. Vérifier le log de generateResultsJob().

Commandes serveur, réouverture et transitions By Year

Entrées communes

Le framework annuel est exposé par des commandes communes et par des wrappers game-specific.

Commandes communes

Wrappers game-specific

Les dossiers ent1/command, hr1/command, mgt1/command, mk1/command et mk2/command exposent typiquement :

Certains jeux ajoutent des wrappers propres, par exemple des mises à jour de rankings.

Réouverture d'année

_utils/command/utils_reopen_year.js est la logique partagée. Une réouverture ne se limite pas à décrémenter currentYear : il faut supprimer/retourner les résultats, nettoyer les évaluations annuelles, logbook/presence associés et remettre l'activité/équipes dans un état cohérent.

Transition activité suivante

utils_start_next_activity.js gère la transition entre activités. Les modes sync/async et le GameUid influencent les messages et l'activité chargée côté clients.

Custom commands

utils_custom_command_by_year.js est le point commun de dispatch lorsque la commande n'est pas directement implémentée dans le wrapper game-specific. Les règles métier restent dans utilsByGameUid[gameUid].

Debug

flowchart LR
UNITY[BackEndBrainCloud] --> WRAP[gameUid/command]
WRAP --> COMMON[_utils/command]
COMMON --> SPEC[game-specific utils]
SPEC --> DOC[activity / teams / results]
DOC --> RTT[act_year_* / result messages]

Ajouter une Custom Command By Year

Les commandes communes comme market research, decisions finished ou financial help passent par utils_custom_command_by_year.js; les décisions métier propres au jeu doivent rester dans les utils game-specific.

Procédure

  1. Définir commandUid et la payload.
  2. Brancher le wrapper <gameUid>_custom_command.
  3. Si commande commune, ajouter le cas dans executeCommandByYear().
  4. Sinon déléguer vers utilsByGameUid[gameUid] / utils custom du jeu.
  5. Modifier teamDecisionsData/teamData/actData au bon niveau.
  6. Sauvegarder les documents concernés.
  7. Envoyer un RTT avec la payload minimale.
  8. Ajouter le modèle/handler Unity.

Réouverture d'année — système destructif

utils_reopen_year.reopenYear() ne doit pas être traité comme un simple changement de state. Il doit remettre en cohérence :

Quand une nouvelle donnée annuelle est ajoutée

Auditer immédiatement reopenYear(), GenerateResults<Game>.deleteResults(), deleteProducts() et les helpers de reset du jeu. Si la donnée existe seulement après génération, elle doit généralement être supprimée/restaurée lors d'une réouverture.

Transition activité suivante

utils_start_next_activity.js porte la logique commune. Un nouveau jeu ou nouveau type d'activité doit être testé avec :

Décisions et génération des résultats By Year

Principe

Le framework By Year ne connaît pas la structure détaillée des décisions. Il délègue à utilsByGameUid[gameUid] et au moteur generateResultsByGameUid[gameUid].

sequenceDiagram
participant U as Unity Decisions UI
participant B as BackEndBrainCloud
participant C as gameUid_custom_command
participant T as teams
participant J as generateResultsJob
participant G as GenerateResultsGame
participant R as results
U->>B: CustomCommand / décision
B->>C: script game-specific
C->>T: sauvegarde TeamDecisions
J->>G: generateCurrentYearResults(actData)
G->>T: lit décisions de toutes les équipes
G->>G: marché / RH / production / features...
G->>R: saveResults
G->>T: met à jour cash/état année suivante

Pattern des moteurs de résultats

Chaque jeu possède un objet GenerateResults* avec généralement :

Factories serveur

utils_factory_generate_results.js instancie le moteur adapté à ent1, hr1, mgt1, mk1 ou mk2. Ajouter un nouveau jeu By Year exige donc de le brancher dans cette factory.

UI Unity

Le socle visuel se trouve dans DecisionsByYearControl, DecisionsPanelByYear et les contrôles SimByYear*. Les écrans de décisions réels sont souvent dans GameSpecific/<gameUid>/Src/UI.

Ajouter une nouvelle décision qui influence les résultats

Ce workflow est le cœur d'une feature By Year.

1. Static data/config

Ajouter la donnée de référence dans la Sheet/JSON game-specific si la décision dépend d'un coût, seuil, segment, feature, pays, employé, etc.

2. Modèle Unity

Ajouter la valeur dans TeamDecisions<Game>/YearData et l'UI qui l'édite.

3. Sauvegarde serveur

Ajouter la custom command game-specific. Valider cash, permissions et contraintes côté serveur, même si l'UI fait déjà la validation.

4. Synchronisation équipe

Émettre un message game-specific afin que les autres membres voient la décision. Ajouter la constante dans GameSpecific/<gameUid>/Src/Messages/MessageType*.cs si nécessaire.

5. Résultats

Consommer la décision dans GenerateResults<Game>. Choisir l'étape correcte : gather → averages → scores → sales/hiring/production → finalize/save.

6. Préparer l'année suivante

Si la décision crée un état durable, vérifier finalizeTeamsData() / reset team pour transporter seulement ce qui doit survivre.

7. Évaluation, ranking, report

Si la nouvelle décision change un score, mettre à jour Eval et ranking. Si elle produit un nouveau résultat, ajouter le modèle Unity et le report.

8. Bots

Pour les jeux async avec bots, décider explicitement quelle valeur le bot choisit. Sinon les bots peuvent générer des résultats invalides ou avoir un avantage/désavantage artificiel.

Debug d'un résultat “bizarre”

  1. Inspecter la décision persistée avant génération.
  2. Inspecter les données agrégées de toutes les équipes.
  3. Inspecter averages/références.
  4. Inspecter score intermédiaire.
  5. Inspecter résultat final.
  6. Inspecter cash/team state après finalizeTeamsData().
  7. Comparer le modèle Unity désérialisé.

Bots et simulations asynchrones By Year

Quand les bots sont utilisés

Dans generateResultsJob(), si actData.isAsync est vrai, takeBotsDecision(actData) est appelé avant la génération des events et résultats.

Jeux avec bots explicites

Le routeur commun instancie actuellement BotsEnt1, BotsMk1 ou BotsMk2. Les équipes marquées isBot reçoivent alors des décisions générées avant le calcul des résultats.

flowchart LR
JOB[generateResultsJob] --> ASYNC{isAsync?}
ASYNC -->|oui| B[takeBotsDecision]
B --> BOT[Bot game-specific]
BOT --> TD[Team decisions]
ASYNC -->|non| EV[Events]
TD --> EV[Events]
EV --> RES[Generate Results]

ENT1

BotsEnt1 gère notamment prêts, capacité de production, construction d'usines, employés, matières premières, choix de pays et création/lancement de produit.

MK1/MK2

Les bots Marketing construisent des décisions de produit/marché selon un niveau et des heuristiques game-specific.

Attention

Le code désactive temporairement l'envoi de messages pendant la boucle bots afin d'éviter de spammer les clients avec chaque décision artificielle. Les documents équipes sont ensuite sauvegardés.

Ajouter ou modifier une décision de bot

Les bots sont exécutés par takeBotsDecision(actData) avant la génération de résultats des activités async. Une nouvelle décision métier qui est obligatoire doit être supportée par le bot du jeu.

Procédure

  1. Ajouter la logique dans Bots<Game>.takeDecisions() ou un helper spécialisé.
  2. Réutiliser les mêmes fonctions de coût/validation que les humains lorsque possible.
  3. Éviter de modifier directement les résultats : le bot doit produire des décisions, puis passer dans le même moteur de génération.
  4. Respecter le botLevel si le jeu simule plusieurs niveaux.
  5. Vérifier cash/stock/capacité après chaque décision en chaîne.
  6. Ajouter une garde si une static data attendue manque.

Jeux actuellement supportés

Le socle crée explicitement des bots pour ENT1, MK1 et MK2. HR1/MGT1 ne doivent pas être supposés bot-compatible sans implémentation dédiée.

Messages pendant les bots

Le framework peut désactiver temporairement certains messages pendant la boucle bot. C'est voulu : on ne veut pas envoyer à chaque client un RTT pour chaque micro-décision artificielle. Vérifier la synchronisation finale après sauvegarde/génération.

Tester un bot

Évaluations, logbook, scénarios et rankings By Year

Évaluations

_utils/utils_eval.js fournit le cadre commun pour activité, présence, logbook, custom evaluations et stubs. Chaque jeu fournit ensuite son Eval<Game> via la factory d'évaluation.

Scores game-specific

Les classes EvalEnt1, EvalHr1, EvalMgt1, EvalMk1 et équivalents calculent notamment scenario score, segment score et activity performance à partir des résultats propres au jeu.

Logbook

Les données de logbook sont persistées dans teamsLogbooks. utils_by_year.deleteYearLogbook() montre que le logbook et ses évaluations sont liés au cycle annuel et doivent être nettoyés correctement lors d'une réouverture/suppression d'année.

Présence

startYear() crée un nouveau stub de présence pour les étudiants. Les helpers setYearPresence() / deleteYearPresence() maintiennent ces données lors des changements d'état.

Scénarios

Le client utilise StudentPlayScenarioByYearState et les pages/scénarios propres au jeu. Le serveur peut calculer le scenario score via la factory d'évaluation game-specific.

Rankings

_utils/utils_rankings.js gère les scores et mises à jour de rankings. Chaque jeu peut appliquer son propre calcul avec des fonctions comme updateRankingsEnt1, updateRankingsHr1, etc.

flowchart LR
RESULTS[Year Results] --> PERF[Act Performance]
SCEN[Scenario] --> EVAL[Student Evaluation]
LOG[Logbook] --> EVAL
PRES[Presence] --> EVAL
PERF --> EVAL
RESULTS --> RANK[Rankings]

UI

EvaluationByYearSyncControl, EvaluationByYearAsyncControl, lignes étudiant, detailed reports, logbook controls, presence popup et ranking controls composent la couche commune.

Ajouter un score ActPerf ou une composante de ranking

ActPerf

  1. Ajouter/calculer la métrique dans les résultats du jeu.
  2. Modifier Eval<Game>.computeActPerfScore().
  3. Ajouter la pondération dans act_perf_pond si la métrique est configurable.
  4. Vérifier le stub envoyé à evaluationTeam.
  5. Ajouter le label/report Unity.
  6. Tester score d'année en cours vs année terminée.

Ranking

Chaque jeu peut définir updateRankings<Game>(). Une nouvelle métrique de ranking doit être calculable depuis la donnée persistée après génération; éviter un ranking basé sur un état transitoire uniquement présent en mémoire.

Scénario

Les réponses scénario sont distinctes des décisions annuelles. Si une nouvelle question/segment influence l'évaluation :

  1. mettre à jour la static data/scenario;
  2. mettre à jour le prefab/page Unity si nouveau type visuel;
  3. vérifier computeScenarioScoreAndSave() ou computeSegmentScoreAndSave() du jeu;
  4. vérifier les Scenario.eScenarioStep uniquement s'ils contrôlent un flow/tutoriel, pas comme stockage des réponses.

Logbook et présence lors d'une nouvelle année

startYear() initialise la présence; reopen doit la supprimer/restaurer correctement. Le logbook est par activité/année et son évaluation est séparée du texte. Toute nouvelle logique annuelle doit conserver ces index d'année.

Debug d'une note globale fausse

  1. Inspecter le résultat game-specific brut.
  2. Appeler computeActPerfScore() avec le même year/team.
  3. Inspecter evaluationTeam.
  4. Inspecter les pondérations simulation/static data.
  5. Inspecter le stub Unity et les caches.

UI et prefabs By Year

Socle commun

Assets/Src/UI/SimByYear/ contient l'UI réutilisée par les jeux annuels. Le contenu métier propre à ENT1/HR1/MGT1/MK1/MK2 reste dans Assets/GameSpecific/<gameUid>/Src/UI et leurs prefabs Resources.

Contrôles principaux

Domaine Classes
Simulation sync/async SimByYearControlBase, SimByYearSyncControl, SimByYearAsyncControl
Années ActivityYearsControl, YearItemControl, WaitSimStartPopupControl
Décisions DecisionsByYearControl, DecisionsPanelByYear
Évaluation EvaluationByYearSyncControl, EvaluationByYearAsyncControl, StudentEval lines
Reports DetailedReport*, ActPerfEval*
Logbook LogBookControl, LogbookEvalPopupControl, LogbookRoleControl
Rankings RankingsPanelControl, HiScoresPopupControl, FinalSequenceRankingsControl
Events EventHistoryPopupControl + rows game-specific

Final sequence

Les contrôles FinalSequence* gèrent la fin de simulation, les équipes, classements et trophées. Ils consomment les rankings/résultats générés côté serveur.

Règle de maintenance

Une UI qui ne connaît ni segment MK, ni employee HR, ni factory ENT, ni feature MGT appartient ici. Dès qu'un écran manipule une structure propre à un jeu, le code et le prefab doivent rester dans GameSpecific.

Ajouter un écran de décision ou un report By Year

Écran réellement commun

Les contrôles de cycle, années, eval, rankings, logbook et final sequence peuvent vivre sous Assets/Src/UI/SimByYear. Une UI qui connaît un type métier game-specific doit rester sous Assets/GameSpecific/<gameUid>/Src/UI.

Nouvel écran de décision game-specific

  1. Créer le contrôle sous GameSpecific/<gameUid>/Src/UI.
  2. Créer/mettre à jour le prefab dans les Resources du jeu.
  3. Lire la décision depuis TeamController<Game>/TeamDecisions<Game>.
  4. Envoyer les changements via la custom command game-specific, pas en modifiant seulement le modèle local.
  5. Écouter le message game-specific pour les modifications d'un coéquipier.
  6. Ajouter les textes.
  7. Ajouter un TutorialUIObject si l'écran participe à un tutoriel.

Nouveau report de résultats

  1. Ajouter le champ dans le modèle Results<Game>.
  2. Créer le contrôle/report UI.
  3. Brancher le report panel/detailed report du jeu.
  4. Gérer année sélectionnée, équipe sélectionnée et teacher/student permissions.
  5. Tester résultat absent (année non générée), année courante et années historiques.

Sync vs async

Les écrans communs ont souvent des variantes SimByYearSyncControl et SimByYearAsyncControl, ainsi que des contrôles d'évaluation sync/async. Avant d'ajouter un bouton “commun”, vérifier s'il est valide dans les deux modes.

Checklist prefab/UI By Year

ENT1 — architecture et logique game-specific

Positionnement

ENT1 est un jeu By Year centré sur une entreprise qui combine production, usines, matières premières, ressources humaines, financement et lancement de produits sur plusieurs pays. Le framework annuel reste commun; les décisions et calculs ENT1 vivent sous GameSpecific/ent1 et server_scripts/ent1.

Static data

exports/exportAllData.js associe ENT1 aux datasets suivants : scenario, salary, employees, raw_mat, factories, product_specs, comms, distrib, medias, events, price_specs, segments, countries et act_perf_pond, plus constants.

Modèles Unity

Domaine Classes principales
Production Factory, FactoryData, RawMatData, ProductionResult
Produits/marché ProductSpecs, LaunchedProduct, Market, Country, DistribData, MediaData, CommTool
RH EmployeesData, SalaryData, ResignationResult
Résultats Results, LaunchProductSales, LaunchProductScores, Ratio
Runtime TeamDecisionsEnt1, TeamEnt1, YearData

Commandes métier serveur

ent1_utils_custom_command.js couvre notamment :

Génération des résultats

flowchart TD
DEC[Décisions équipes] --> PROD[Usines + matières premières]
DEC --> HR[Employés + salaires]
DEC --> LP[Produits lancés]
PROD --> CAP[Capacité / production]
HR --> CAP
LP --> MARKET[Marché par pays/segment]
CAP --> SALES[Ventes]
MARKET --> SALES
SALES --> FIN[Cash / résultats]
FIN --> SCORE[Scores + ratios + rankings]

GenerateResultsEnt1 rassemble produits et usines, calcule moyennes, démissions, production, scores et ventes, puis finalise les équipes et sauvegarde les résultats.

Bots

BotsEnt1 peut décider prêts, production souhaitée, construction d'usines, employés, choix de pays, matières premières et création de produits. Il est utilisé dans les simulations async.

UI

Les écrans game-specific sont sous Assets/GameSpecific/ent1/Src/UI : usines, production, raw materials, RH, salaires, finance, pays, portfolio, lancement produit, marché, rapports et checklists.

Debug

Pour un résultat ENT1 incorrect, suivre : custom command → TeamDecisionsEnt1 → GenerateResultsEnt1 → Results. Pour une incohérence de production, inspecter ensemble factories, raw mats, employés et la quantité réellement lancée.

Ajouter une feature ENT1 de A à Z

ENT1 croise plusieurs sous-systèmes; une décision peut affecter production, matières premières, employés, cash, produits et ventes. Avant de coder, identifier laquelle de ces chaînes la feature modifie.

Carte serveur à connaître

Zone Fonctions clés
Cycle/état startYearGameSpec, getYearData, generateEvents, anyDecisionsTaken.
Usines / production getTotalFactoryCost, getNeededRawMats, SetProductionsForProduct, computeFactoriesProduction.
RH getNumEmployees, computeSalaryAverages, computeResignations.
Marché gatherProducts, computeProductAverages, computeScores, generateSales.
Fin de génération finalizeTeamsData, saveResults, resetTeam, deleteResults.

Custom commands ENT1 existantes

Le fichier ent1_utils_custom_command.js gère notamment saveLaunch, deleteLaunch, buildFactory, sellFactory, buyRawMats, setFactoryProduction, hireEmployees, dismissEmployees, setEmployeeSalary et updateLoan.

Procédure pour une nouvelle décision

  1. Ajouter la static data si la décision dépend d'un coût/capacité/pays/employee.
  2. Ajouter le champ au YearData/TeamDecisionsEnt1.
  3. Ajouter l'UI dans le domaine approprié : factory, HR, finance, product/market.
  4. Ajouter la command et valider cash/capacité côté serveur.
  5. Envoyer un message ENT1 et mettre à jour TeamControllerEnt1.
  6. Consommer la décision dans la bonne phase de GenerateResultsEnt1.
  7. Si l'état survit à l'année, mettre à jour finalizeTeamsData/resetTeam.
  8. Si score/ratio change, mettre à jour Eval/rankings/report.
  9. Si bots supportés, ajouter la décision dans BotsEnt1.
  10. Tester reopen year.

Piège principal : dépendances physiques

Une décision de lancement produit peut être impossible même si le produit est valide : il faut production disponible, usines, matières premières et employés. Les validations UI ne remplacent jamais celles côté serveur.

Debug génération ENT1

  1. team decisions;
  2. factories + raw mats;
  3. employees/salaries;
  4. production calculée;
  5. products gathered;
  6. averages/scores;
  7. sales;
  8. cash/ratios finalisés;
  9. Results Unity/report.

HR1 — architecture et logique game-specific

Positionnement

HR1 est le jeu By Year orienté gestion des ressources humaines : staffing, rémunération, avantages, postings, contrats, CEO et segments d'employés.

Static data

Les datasets importés incluent ceo, employees, strategies, advantages, events, scenario, segments, contracts et act_perf_pond, plus constants.

Modèles Unity

Hooks serveur

GameSpecificUtilsHr1 gère entre autres le démarrage d'année, génération de contrats, reset des avantages, events, CEOs, coûts d'avantages, coûts de postings/licenciements, coûts employés et rankings.

Génération

flowchart LR
DEC[Staffing + rémunération + avantages + postings] --> HIRE[Hiring]
DEC --> RET[Retention / belonging]
DEC --> COST[Employee costs]
DEC --> CEO[CEO]
HIRE --> RES[Segment Results]
RET --> RES
COST --> RES
CEO --> RES
RES --> SCORE[Scores / ActPerf]

GenerateResultsHr1 finalise contrats et CEOs, calcule hiring, averages, retention/belonging, scores de postings et segments, puis sauvegarde les résultats.

Évaluation

EvalHr1 fournit scenario score, segment score et performance annuelle. Les résultats HR alimentent ensuite le cadre commun d'évaluation By Year.

UI

Les vues spécifiques comprennent CEO, staffing, employés, avantages, stratégies, postings, contrats, rémunération et rapports. Les classes sont sous Assets/GameSpecific/hr1/Src/UI.

Debug

Pour un problème RH, isoler d'abord la décision (employee/segment/posting/advantage), puis le coût calculé dans hr1_utils.js, puis la phase correspondante de GenerateResultsHr1.

Ajouter une feature HR1 de A à Z

Custom commands réelles

hr1_custom_command.js route actuellement :

Ces commandes ne font pas que sauvegarder une valeur : elles recalculent souvent un coût, modifient cash, sauvegardent teams, parfois actDataGameSpec, puis envoient des RTT comme posting_changed, salary_changed, team_new_bid.

Carte résultats

Ajouter une nouvelle décision RH

  1. Définir si la décision appartient au segment, à l'employé, aux avantages, aux postings, CEO ou contrats.
  2. Ajouter la donnée au bon niveau de TeamDecisionsHr1/YearData.
  3. Ajouter l'UI sous GameSpecific/hr1/Src/UI.
  4. Ajouter la branche dans hr1_custom_command.js.
  5. Calculer le coût avec un helper utilsHr1 partagé; ne pas dupliquer le calcul UI.
  6. Appliquer/refund le delta de cash correctement lors d'une édition/suppression.
  7. Envoyer le RTT approprié.
  8. Consommer la décision dans GenerateResultsHr1.
  9. Adapter EvalHr1.computeActPerfScore() si le score change.
  10. Vérifier reopen/delete results.

CEO bids : exemple d'opération multi-document

Un bid CEO modifie à la fois teams et un compteur dans actDataGameSpec.ceos via saveDocumentIncrementData(). Une nouvelle feature concurrentielle partagée entre équipes peut nécessiter le même pattern; il faut alors tester la concurrence entre plusieurs teams.

Debug HR1

Quand un coût est faux : comparer static data → helper de coût → delta ancien/nouveau → cash sauvegardé. Quand le résultat annuel est faux : comparer segment decisions → averages → hiring/retention → result segment → ActPerf.

MGT1 — architecture et logique game-specific (DEPRECATED)

Positionnement

MGT1 modélise la gestion d'une équipe de développement : employés, temps de travail, features, segments, budget et lancement d'un jeu.

Static data

Les datasets comprennent employees, events, features, scenario, segments et act_perf_pond, plus constants.

Modèles Unity

Logique serveur

GameSpecificUtilsMgt1 gère notamment :

Résultats

GenerateResultsMgt1 calcule le travail requis, les bonus énergie/discipline/bonheur, les employés à inactiver, les résultats par équipe et la transition des employés vers l'année suivante.

flowchart LR
EMP[Employés] --> WORK[Temps de travail]
FEAT[Features] --> NEED[Travail requis]
WORK --> PROG[Progression features]
NEED --> PROG
PROG --> COMPLETE[Features complétées]
COMPLETE --> GAME[Résultat du jeu]
EMP --> BONUS[Energy / discipline / happiness]
BONUS --> GAME

UI

L'UI game-specific contient employees list, team leader, producer, president, feature selectors, budgets, work allocation, lancement du jeu et rapports.

Debug

Pour une feature qui progresse mal, vérifier setWorkOnFeature(), le temps/coût de l'employé, getNeededWork() et les bonus appliqués dans la génération annuelle.

Ajouter une feature MGT1 de A à Z

Custom commands réelles

mgt1_custom_command.js gère :

Carte du moteur

Ajouter une nouvelle feature de jeu MGT1

  1. Ajouter la static data sous features si c'est une feature configurable.
  2. Ajouter le modèle Unity Feature/GameFeatureData si structure nouvelle.
  3. Ajouter le contrôle UI et les ratios/budget si nécessaire.
  4. Ajouter la command de création/édition.
  5. Vérifier les employés déjà assignés lors d'une suppression : deleteEmployeeFeature() doit nettoyer les travaux devenus invalides.
  6. Ajouter la contribution au workNeeded et à computeTeamsResults().
  7. Vérifier la complétion via completeFeature().
  8. Ajouter le résultat/report.

Employés : index et noms

Le code associe certains employés par index dans yearData.employees et par nom dans employeesNames. Toute opération d'ajout/suppression doit préserver l'alignement attendu; une suppression mal gérée peut faire pointer un nom vers le mauvais employé.

Work time : validation serveur obligatoire

changeWorkTime() valide game, feature, task level et niveau réel de l'employé. Une nouvelle allocation de travail doit conserver ce type de validation côté serveur.

Debug MGT1

Pour une feature qui ne progresse pas : feature exists → employee works structure → level/workTime → needed work → bonuses → computeTeamsResults → final result.

MK1 — architecture et logique game-specific

Positionnement

MK1 est le moteur marketing By Year centré sur création de produit, choix de segment, prix, distribution, communication et lancement sur le marché.

Static data

Les datasets comprennent scenario, segments, product_specs, shops, price_specs, medias, comms, distrib, events et act_perf_pond, plus constants.

Modèles Unity

Logique serveur

GameSpecificUtilsMk1 gère sauvegarde/suppression de produit, lancement, rentabilité, conversion cash↔quantité, sélection de segment, randomisation des décisions bots, événements et création/lancement de produit.

Résultats

flowchart LR
P[Produits] --> G[gatherProducts]
SEG[Segments] --> G
G --> AVG[Market averages]
AVG --> PS[Product score]
AVG --> CS[Communication score]
AVG --> DS[Distribution score]
PS --> SALES[Sales]
CS --> SALES
DS --> SALES
SALES --> TEAM[Cash / Results]

GenerateResultsMk1 calcule les moyennes de marché, scores produit/communication/distribution, ventes et résultats annuels.

Bots

BotsMk1 utilise les heuristiques du moteur MK1 pour générer les décisions d'équipes artificielles en async.

UI

Les écrans spécifiques couvrent portfolio, nouveaux produits, launched products, market, communications, media, shops, calculator, lancement et rapports. Les tutoriels MK1 couvrent notamment création du premier produit et premier lancement.

Ajouter une feature MK1 de A à Z

Entrées de décision principales

mk1_custom_command.js route :

Pipeline de résultats

flowchart LR
DEC[Products + launches] --> G[gatherProducts]
G --> AVG[computeAverages]
AVG --> PS[getProductScore]
AVG --> CS[getCommScore]
AVG --> DS[getDistScore]
PS --> SCORE[computeScores]
CS --> SCORE
DS --> SCORE
SCORE --> SALES[generateSales]
SALES --> SAVE[saveResults / finalizeTeamsData]

Ajouter un nouveau critère marketing

  1. Ajouter la static data (segment/product spec/media/distrib/etc.).
  2. Ajouter la propriété dans Product ou LaunchedProduct selon qu'elle est produit ou lancement.
  3. Ajouter le champ UI dans NewProductControl/LaunchProductControl ou un nouveau contrôle.
  4. Transmettre la valeur via save_product ou save_launched_product.
  5. Ajouter le coût dans getProductCost()/getLaunchProductCost() si applicable.
  6. Ajouter la valeur au score pertinent : produit, comm ou distribution.
  7. Vérifier averages et normalisation si le score est relatif aux concurrents.
  8. Vérifier generateSales().
  9. Ajouter le report Unity et éventuellement ActPerf/ranking.
  10. Adapter BotsMk1.takeDecisions() si le critère est requis.

Calculateur vs décision réelle

MK1 contient des produits de calculateur en plus des produits réellement sauvegardés/lancés. Une nouvelle propriété doit être cohérente dans les deux chemins si le calculateur est censé la simuler. Auditer saveProduct, saveLaunchedProduct et deleteCalcProduct.

Debug ventes MK1

  1. produit/lancement persisted;
  2. segment cible;
  3. averages marché;
  4. product score;
  5. comm score;
  6. distribution score;
  7. sales generated;
  8. cash/result saved;
  9. report Unity.

Notifications et présence

Notifications envoyées depuis Unity

Le Game envoie les notifications au backend Web. NotificationManager.SaveNotification() lance la coroutine BackEndBase.SaveNotification(), qui effectue POST /saveNotification avec userUid, notificationType, isImportant et extraData.

userUid est le profil jeu du destinataire (products_code.game_oid); extraData.simUid identifie la simulation. NotificationController, protégé par AuthGuardUser, délègue à GameNotificationService.notifyGameProfile().

Le serveur vérifie le format, exclut les types réservés au serveur et contrôle que l'expéditeur et le destinataire appartiennent à la simulation BrainCloud. Le profil jeu destinataire est ensuite converti en compte Web pour NotificationService.notify().

La notification est enregistrée dans l'inbox Redis avant son annonce aux connexions du compte. Le push est tenté ensuite; un échec du push n'annule pas l'entrée enregistrée. L'inbox possède une limite de taille et une durée de rétention configurées dans NOTIFICATION_CONFIG.

Le Web charge GET /notifications, marque une entrée comme lue via POST /notifications/:notificationUid/read et la supprime via DELETE /notifications/:notificationUid. La réception live utilise notification.created sur le socket du compte /account.

Voir Notifications et Push pour le système Web, et Panel Web — interface superposée au jeu et intégration Unity pour son affichage au-dessus du jeu.

Trois mécanismes de présence

Mécanisme Donnée et usage Code
Présence du compte Web Statut online/away et jeu actif du compte; utilisé par les interfaces Web. AccountSocketService, socket /account, updateStatus et updateGamePresence.
Heartbeat du Game Session du jeu et derniers heartbeats utilisés par l'UI Unity pour les utilisateurs de la simulation. User.DoHeartbeat(), endpoint /heartBeat et UsersLastHeartbeats.
Présence évaluée By Year Participation enregistrée par étudiant, activité et année, utilisée pour calculer un score. _command/set_presence.js et evaluation.presence[actUid][yearId].

Le nouveau statut Web ne remplace pas automatiquement les deux usages Game : le code local conserve des appels et des consommateurs distincts.

Présence Web du compte

AccountSocketService suit l'activité du navigateur, le focus et l'inactivité pour émettre online ou away. PageWebglComponent appelle setGamePresence(pid, gameUid) après validation de l'accès au jeu, puis clearGamePresence() à sa destruction.

À la reconnexion du socket, le service réémet le jeu actif conservé en mémoire. Cette information désigne le jeu ouvert; elle ne crée pas d'entrée de présence pédagogique dans BrainCloud.

Heartbeat utilisé par Unity

LoadUserState démarre le heartbeat. User.DoHeartbeat() utilise HEARTBEAT_RATE_SECONDS = 5 : l'intervalle d'attente est de 5 secondes entre les appels. La réponse peut mettre à jour les derniers heartbeats dans le controller de simulation.

SimulationBaseController.IsUserOnline() et ActivityBaseController.IsUserOnline() considèrent un utilisateur online si le dernier heartbeat date de moins de 5 × 12 = 60 secondes. ProfileImageControl consomme cet état pour l'affichage. Le heartbeat a donc encore un consommateur Unity.

Présence évaluée By Year

BackEndBrainCloud.SetPresence() appelle _command/set_presence. Le script traite seulement les utilisateurs avec isTeacher == 0, charge leur simulation et l'activité courante, puis enregistre la présence uniquement si currentYearState == 1.

setYearPresence() écrit true dans evaluation.presence[actUid][yearId] si l'entrée n'existe pas déjà, sauvegarde le document et envoie user_new_presence_eval à l'étudiant et au professeur.

getPresenceScore() compte les années enregistrées entre 0 et yearId, puis divise le cumul de 100 points par le nombre d'années. StudentState et ActivityByYearController appellent encore SetPresence() dans leurs transitions.

startYear() envoie le stub de score à chaque étudiant; il ne marque pas tous les étudiants présents. L'enregistrement effectif relève de set_presence.

Réouverture annuelle

utils_reopen_year.js appelle deleteYearPresence(student, actData.currentYear + 1, actData) après ajustement de l’année courante. Ce flux appartient au cycle des évaluations By Year.

Usages conservés dans le Game

Le heartbeat alimente l'UI Unity, et la présence By Year alimente les évaluations BrainCloud. La présence du compte Web décrit le compte connecté et son jeu actif; ces deux mécanismes Game conservent leurs consommateurs propres.

Sources vérifiées

Game : Assets/Src/Game/Notifications/NotificationManager.cs, Game/BackEnd/BackEndBase.cs, BackEndBrainCloud.cs, Game/User.cs, Game/GameStates/StudentState.cs, LoadUserState.cs, les controllers de simulation/activité et UI/ProfileImageControl.cs. BrainCloud : _command/set_presence.js, _utils/utils_by_year.js, utils_eval.js et _utils/command/utils_reopen_year.js.

Web : uniksim-back-end/src/app.controller.ts, service/game-notification.service.ts, notification.service.ts; frontend : service/account/account-socket.service.ts et Pages/PageWebgl/PageWebgl.component.ts.

Vérification par lecture du code Game et BrainCloud de la branche refactor_notifications, et du code Web de la branche messagerie_fixes, le 30 septembre 2026. Aucun envoi de notification ou changement de présence réel n'a été exécuté pour cette vérification.

MK2 — architecture et logique game-specific

Positionnement

MK2 reprend le cœur marketing de MK1 mais ajoute notamment la logique pays/segments, relance, stock, loyauté et notoriété. Il doit donc être traité comme un moteur distinct plutôt qu'une simple skin MK1.

Static data

Les datasets comprennent product_specs, comms, distrib, medias, events, price_specs, scenario, segments, countries, shops et act_perf_pond, plus constants.

Modèles Unity

Les classes sont proches de MK1 mais ajoutent notamment Country. L'UI ajoute CountryLaunchControl et des variantes MK2 du marché et des rapports.

Logique serveur spécifique

Résultats

flowchart TD
P[Products by country/segment] --> AVG[Market averages]
AVG --> SCORE[Product + Comm + Distrib]
SCORE --> SALES[Sales]
SALES --> LOY[Loyalities]
LOY --> NOT[Notoriety]
NOT --> RES[Team Results]
SALES --> STOCK[Stock / redistribution]
STOCK --> RES

GenerateResultsMk2 ajoute notamment computeLoyalities() et persiste la notoriété en plus des ventes et résultats.

Bots

BotsMk2 génère les décisions marketing async en tenant compte des règles MK2.

Debug

Si un résultat diffère de MK1, vérifier d'abord les branches pays/relaunch/loyalty/stock avant de comparer les formules de score communes en apparence.

Ajouter une feature MK2 sans supposer MK1

MK2 partage des concepts avec MK1, mais son moteur n'est pas un alias. Il possède son propre GameSpecificUtilsMk2, son propre générateur de résultats et ses propres modèles Unity.

Fonctions spécifiques MK2 à connaître

Custom commands

Les noms d'entrée sont similaires à MK1 : save_product, save_launched_product, delete product/launch/calc. Ils appellent toutefois utilsMk2, dont les structures incluent notamment country/stock/relaunch.

Ajouter un nouveau comportement de lancement

  1. Définir si le changement concerne produit, lancement initial, relaunch, country ou stock.
  2. Ajouter la donnée dans Product/LaunchedProduct/Country côté Unity.
  3. Ajouter la saisie UI, notamment CountryLaunchControl si géographique.
  4. Ajouter la valeur aux save commands.
  5. Adapter launchTypeCalculation/relaunchSegment/launchCountryCalculation selon le cas.
  6. Vérifier les refunds/redistribution de stock lors d'une modification/suppression.
  7. Ajouter l'effet aux scores/ventes.
  8. Vérifier loyalty + notoriety sur l'année suivante.
  9. Adapter BotsMk2.

Stock MK2

Le stock de lancement MK2 est distinct du système d'inventaire Accounting. Ne pas réutiliser mentalement les règles FIFO/periodic stock de By Module : ici il s'agit de quantités marketing liées aux lancements et à la redistribution/refund.

Debug différence MK1/MK2

Quand MK2 donne un résultat différent d'une intuition basée sur MK1, vérifier d'abord country, relaunch, stock, loyalities et notoriety. Ensuite seulement comparer product/comm/dist score.

Test de régression MK2