Hints used to be 11 hardcoded PHP strings/thresholds in
SendMainframeHintsCommand, walked through a fixed if/else chain. They
are now Hint rows (game, condition, thresholdSeconds, message,
sortOrder) manageable at /admin/games/{game}/hints - freely
addable, removable, and reorderable per game, which sets up exactly
what's needed for difficulty-dependent hint timing later (Easy/Medium/
Hard are separate games, so each gets its own hint set).
The condition a hint fires behind (e.g. "hasn't used help yet",
"isn't verified yet") is still a fixed, code-defined check
(HintCondition enum) tied to real game state - a true free-form
condition editor would need a full rules engine, which is out of
scope. What's fully free-form is which hints exist, in what order,
gated behind which of those conditions, with what wording and timing.
SendMainframeHintsCommand now reads hints from the DB: walking them in
sortOrder, it finds the first distinct condition still unresolved for
a player, fires the most-escalated hint sharing that condition whose
threshold has passed, and stops - matching the original "stop at the
first unresolved, dependent step" behavior.
Migration seeds every existing game with the previous hardcoded
hints so behavior doesn't regress on deploy. Two minor fidelity
simplifications from the original hardcoded logic, called out here
since they're easy to miss: the "go to rapports directory" hint no
longer additionally checks whether the player is already in that
folder, and the two chat hints are now independent conditions
(broadcast done / contacted everyone privately) instead of one
combined check - both are edge-case-only behavior changes, not
regressions in the common path.
79 lines
4.6 KiB
Twig
79 lines
4.6 KiB
Twig
{% extends 'game/admin/base.html.twig' %}
|
|
|
|
{% block title %}Hints — {{ game.name }} — Admin{% endblock %}
|
|
|
|
{% block admin_body %}
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<a href="{{ path('game_admin_games') }}" style="color: #64748b; text-decoration: none; font-size: 0.9rem;">← Games</a>
|
|
</div>
|
|
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
|
|
<h1 style="margin: 0; font-size: 1.5rem; color: #0f172a;">Hints — {{ game.name }}</h1>
|
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
<span style="color: #64748b; font-size: 0.9rem;">{{ hints|length }} total</span>
|
|
<a href="{{ path('game_admin_hint_new', {game: game.id}) }}" style="
|
|
padding: 0.5rem 1rem;
|
|
background: #3b82f6;
|
|
color: #fff;
|
|
border-radius: 6px;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
">+ New Hint</a>
|
|
</div>
|
|
</div>
|
|
|
|
<p style="color: #64748b; font-size: 0.85rem; margin-top: -1rem; margin-bottom: 1.5rem;">
|
|
Hints are checked in order. For each row, if its condition is still unresolved for a player, the
|
|
highest threshold reached among hints sharing that same condition fires and the check stops there
|
|
for that pass — later rows are only reached once every condition before them is resolved.
|
|
</p>
|
|
|
|
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); overflow: hidden;">
|
|
<table style="width: 100%; min-width: 760px; border-collapse: collapse; font-size: 0.9rem;">
|
|
<thead>
|
|
<tr style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0;">
|
|
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Order</th>
|
|
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Condition</th>
|
|
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Threshold</th>
|
|
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Message</th>
|
|
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for hint in hints %}
|
|
<tr style="border-bottom: 1px solid #f1f5f9;">
|
|
<td style="padding: 0.75rem 1rem; color: #94a3b8;">{{ hint.sortOrder }}</td>
|
|
<td style="padding: 0.75rem 1rem; color: #0f172a;">{{ hint.condition.label }}</td>
|
|
<td style="padding: 0.75rem 1rem; color: #475569; white-space: nowrap;">{{ (hint.thresholdSeconds / 60)|round(1, 'floor') }} min</td>
|
|
<td style="padding: 0.75rem 1rem; color: #475569; max-width: 420px;">{{ hint.message|length > 90 ? hint.message|slice(0, 90) ~ '…' : hint.message }}</td>
|
|
<td style="padding: 0.75rem 1rem; white-space: nowrap;">
|
|
<a href="{{ path('game_admin_hint_edit', {game: game.id, id: hint.id}) }}" style="
|
|
color: #3b82f6;
|
|
text-decoration: none;
|
|
font-size: 0.85rem;
|
|
margin-right: 0.75rem;
|
|
">Edit</a>
|
|
<form method="post" action="{{ path('game_admin_hint_delete', {game: game.id, id: hint.id}) }}" style="display: inline;" onsubmit="return confirm('Delete this hint?')">
|
|
<input type="hidden" name="_token" value="{{ csrf_token('delete_hint_' ~ hint.id) }}">
|
|
<button type="submit" style="
|
|
background: none;
|
|
border: none;
|
|
color: #ef4444;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
padding: 0;
|
|
">Delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="5" style="padding: 2rem; text-align: center; color: #94a3b8;">No hints configured for this game yet.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endblock %}
|