Skip to content

Commit 260be38

Browse files
committed
Fix duel opponent resolution and blessing UX
1 parent 3481917 commit 260be38

5 files changed

Lines changed: 133 additions & 36 deletions

File tree

app/js/i18n/en.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ var LangEN = {
210210
chronicle_placeholder: 'Write your words into the World...',
211211
chronicle_send: 'Inscribe',
212212
chronicle_empty: 'Chronicle is empty. Be the first to write!',
213+
chronicle_bless_posts_only_hint: 'Blessings are available only through Chronicle posts. There are no suitable posts from other mages in the feed yet.',
214+
chronicle_no_bless_targets_hint: 'There are no other mages\' posts to bless right now. Blessings work only on other players\' Chronicle posts.',
213215
chronicle_narrative_hunt: '{name} set out on a hunt.',
214216
chronicle_narrative_hunt_win: '{name} defeated {creature}!',
215217
chronicle_narrative_hunt_lose: '{name} fell in battle with {creature}.',

app/js/i18n/ru.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ var LangRU = {
210210
chronicle_placeholder: 'Впиши слова в Мир...',
211211
chronicle_send: 'Вписать',
212212
chronicle_empty: 'Хроника пуста. Будь первым, кто впишет слова!',
213+
chronicle_bless_posts_only_hint: 'Благословения доступны только через записи в Хронике. Пока в ленте нет подходящих постов других магов.',
214+
chronicle_no_bless_targets_hint: 'Сейчас в ленте нет чужих записей для благословения. Благословлять можно только посты других магов в Хронике.',
213215
chronicle_narrative_hunt: '{name} отправился на охоту.',
214216
chronicle_narrative_hunt_win: '{name} одолел {creature}!',
215217
chronicle_narrative_hunt_lose: '{name} пал в бою с {creature}.',

app/js/ui/screens/chronicle.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,22 @@ var ChronicleScreen = (function() {
238238
return (b.blockNum || 0) - (a.blockNum || 0);
239239
});
240240

241+
var user = VizAccount.getCurrentUser();
242+
var blessableCount = 0;
243+
for (var b = 0; b < entries.length; b++) {
244+
if (entries[b].account && entries[b].account !== user) blessableCount++;
245+
}
246+
241247
if (entries.length === 0) {
242-
feed.innerHTML = '<p class="empty-state">' + Helpers.t('chronicle_empty') + '</p>';
248+
feed.innerHTML = '<p class="empty-state">' + Helpers.t('chronicle_empty') + '</p>' +
249+
'<p class="chronicle-hint">' + Helpers.t('chronicle_bless_posts_only_hint') + '</p>';
243250
return;
244251
}
245252

246253
var html = '';
254+
if (blessableCount === 0) {
255+
html += '<p class="chronicle-hint">' + Helpers.t('chronicle_no_bless_targets_hint') + '</p>';
256+
}
247257
for (var k = 0; k < entries.length; k++) {
248258
html += _renderEntry(entries[k]);
249259
}
@@ -289,19 +299,27 @@ var ChronicleScreen = (function() {
289299
var icon = _getActionIcon(entry.actionType);
290300
var timeStr = entry.timestamp ? Helpers.timeAgo(entry.timestamp) : '';
291301

302+
var user = VizAccount.getCurrentUser();
303+
var canBless = entry.account && user && entry.account !== user;
304+
var actionsHtml = '';
305+
306+
if (canBless) {
307+
actionsHtml = '<div class="chronicle-actions">' +
308+
'<button class="bless-button" data-account="' + (entry.account || '') + '" ' +
309+
'aria-label="' + Helpers.escapeHtml(t('chronicle_bless_cost', { cost: Helpers.manaCost(BLESS_ENERGY), name: charName })) + '">' +
310+
'✨ ' + t('chronicle_bless') + ' · ' + Helpers.manaCost(BLESS_ENERGY) +
311+
'</button>' +
312+
'</div>';
313+
}
314+
292315
return '<article class="chronicle-entry" role="article" aria-label="' + Helpers.escapeHtml(charName) + '">' +
293316
'<div class="chronicle-entry-header">' +
294317
'<span class="chronicle-icon" aria-hidden="true">' + icon + '</span>' +
295318
'<strong class="chronicle-author">' + Helpers.escapeHtml(charName) + '</strong>' +
296319
(timeStr ? '<span class="chronicle-time">' + timeStr + '</span>' : '') +
297320
'</div>' +
298321
'<p class="chronicle-text">' + Helpers.escapeHtml(entry.text) + '</p>' +
299-
'<div class="chronicle-actions">' +
300-
'<button class="bless-button" data-account="' + (entry.account || '') + '" ' +
301-
'aria-label="' + Helpers.escapeHtml(t('chronicle_bless_cost', { cost: Helpers.manaCost(BLESS_ENERGY), name: charName })) + '">' +
302-
'✨ ' + t('chronicle_bless') + ' · ' + Helpers.manaCost(BLESS_ENERGY) +
303-
'</button>' +
304-
'</div>' +
322+
actionsHtml +
305323
'</article>';
306324
}
307325

app/js/ui/screens/duel.js

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var DuelScreen = (function() {
1010
var STATE_POLL_INTERVAL_MS = 3000;
1111
var registeredListeners = false;
1212
var _statePollTimer = null;
13+
var _hydratingAccounts = {};
1314

1415
var duelState = _createInitialState();
1516

@@ -47,6 +48,7 @@ var DuelScreen = (function() {
4748
if (opts.combatRef) duelState.combatRef = String(opts.combatRef);
4849
if (opts.challengeData) duelState.challengeData = opts.challengeData;
4950

51+
_ensureCharacterHydrated(duelState.opponent);
5052
_syncFromState();
5153
_ensureEventListeners();
5254

@@ -129,10 +131,14 @@ var DuelScreen = (function() {
129131
function _renderPreDuel(el) {
130132
var t = Helpers.t;
131133
var user = VizAccount.getCurrentUser();
132-
var myChar = StateEngine.getCharacter(user) || {};
133-
var oppChar = StateEngine.getCharacter(duelState.opponent) || {};
134-
var myDuels = _getPlayerStats(user);
135-
var oppDuels = _getPlayerStats(duelState.opponent);
134+
var participants = _getResolvedParticipants();
135+
var myAccount = participants.me || user;
136+
var oppAccount = participants.opponent || duelState.opponent;
137+
var myChar = StateEngine.getCharacter(myAccount) || {};
138+
var oppChar = StateEngine.getCharacter(oppAccount) || {};
139+
duelState.opponent = oppAccount || duelState.opponent;
140+
var myDuels = _getPlayerStats(myAccount);
141+
var oppDuels = _getPlayerStats(oppAccount);
136142
var pendingNotice = '';
137143
var beginLabel = duelState.combatRef ? t('duel_begin') : t('duel_begin_real');
138144

@@ -149,14 +155,14 @@ var DuelScreen = (function() {
149155
'<div class="duel-matchup">' +
150156
'<div class="duel-mage duel-mage-left">' +
151157
'<span class="duel-mage-icon" aria-hidden="true">' + Helpers.classIcon(myChar.className) + '</span>' +
152-
'<strong>' + Helpers.escapeHtml(myChar.name || user || t('duel_you')) + '</strong>' +
158+
'<strong>' + Helpers.escapeHtml(myChar.name || myAccount || t('duel_you')) + '</strong>' +
153159
'<span class="duel-mage-info">' + t('home_level') + ' ' + (myChar.level || 1) + '</span>' +
154160
'<span class="duel-mage-info">' + t('duel_wins') + ': ' + myDuels.wins + '</span>' +
155161
'</div>' +
156162
'<span class="duel-vs" aria-hidden="true">VS</span>' +
157163
'<div class="duel-mage duel-mage-right">' +
158164
'<span class="duel-mage-icon" aria-hidden="true">' + Helpers.classIcon(oppChar.className) + '</span>' +
159-
'<strong>' + Helpers.escapeHtml(oppChar.name || duelState.opponent || '???') + '</strong>' +
165+
'<strong>' + Helpers.escapeHtml(oppChar.name || oppAccount || '???') + '</strong>' +
160166
'<span class="duel-mage-info">' + t('home_level') + ' ' + (oppChar.level || 1) + '</span>' +
161167
'<span class="duel-mage-info">' + t('duel_wins') + ': ' + oppDuels.wins + '</span>' +
162168
'</div>' +
@@ -190,7 +196,7 @@ var DuelScreen = (function() {
190196

191197
if (typeof BattleNarrator !== 'undefined') {
192198
BattleNarrator.announce(t('duel_narrator_pre', {
193-
opponent: oppChar.name || duelState.opponent
199+
opponent: oppChar.name || oppAccount
194200
}));
195201
}
196202
}
@@ -724,6 +730,9 @@ var DuelScreen = (function() {
724730
duelState.currentRound = duel.currentRound || duelState.currentRound;
725731

726732
var user = VizAccount.getCurrentUser();
733+
duelState.opponent = _resolveOpponentFromDuel(duel, user, duelState.opponent);
734+
_ensureCharacterHydrated(duel.challenger);
735+
_ensureCharacterHydrated(duel.target);
727736
duelState.opponentCommitted = _hasOpponentCommitted(duel, user, duelState.currentRound);
728737

729738
if (duel.roundResults && duel.roundResults.length) {
@@ -756,6 +765,64 @@ var DuelScreen = (function() {
756765
}
757766
}
758767

768+
function _getResolvedParticipants() {
769+
var user = VizAccount.getCurrentUser();
770+
var duel = _getCurrentDuel();
771+
return {
772+
me: user,
773+
opponent: _resolveOpponentFromDuel(duel, user, duelState.opponent)
774+
};
775+
}
776+
777+
function _resolveOpponentFromDuel(duel, user, fallbackOpponent) {
778+
if (!duel || !user) return fallbackOpponent || '';
779+
if (duel.challenger === user) return duel.target || fallbackOpponent || '';
780+
if (duel.target === user) return duel.challenger || fallbackOpponent || '';
781+
return fallbackOpponent || duel.target || duel.challenger || '';
782+
}
783+
784+
function _ensureCharacterHydrated(account) {
785+
if (!account || StateEngine.getCharacter(account) || _hydratingAccounts[account]) return;
786+
if (typeof VizAccount === 'undefined' || typeof VizAccount.getAccount !== 'function') return;
787+
if (typeof CharacterSystem === 'undefined' || typeof CharacterSystem.createCharacter !== 'function') return;
788+
789+
_hydratingAccounts[account] = true;
790+
VizAccount.getAccount(account, function(err, accountData) {
791+
delete _hydratingAccounts[account];
792+
if (err || !accountData || StateEngine.getCharacter(account)) return;
793+
794+
var grimoire = VizAccount.parseGrimoire(accountData);
795+
if (!grimoire || !grimoire.class) return;
796+
797+
var state = StateEngine.getState();
798+
var character = CharacterSystem.createCharacter(account, grimoire.name || account, grimoire.class);
799+
if (!character) return;
800+
801+
if (grimoire.level && grimoire.level > 1) {
802+
character.level = grimoire.level;
803+
character.xp = grimoire.xp || 0;
804+
if (typeof GameFormulas !== 'undefined' && GameFormulas.calculateMaxHp && CharacterSystem.getTotalStat) {
805+
character.hp = GameFormulas.calculateMaxHp(character.className, character.level, CharacterSystem.getTotalStat(character, 'res'));
806+
character.maxHp = character.hp;
807+
}
808+
}
809+
810+
if (VizAccount.getEffectiveShares && CharacterSystem.updateCoreBonus) {
811+
var effectiveShares = VizAccount.getEffectiveShares(accountData);
812+
var cappedShares = Math.min(effectiveShares, 1000000000000);
813+
CharacterSystem.updateCoreBonus(character, cappedShares);
814+
}
815+
816+
state.characters[account] = character;
817+
state.inventories[account] = state.inventories[account] || [];
818+
state.quests[account] = state.quests[account] || (typeof QuestSystem !== 'undefined' && QuestSystem.createPlayerQuestState
819+
? QuestSystem.createPlayerQuestState()
820+
: {});
821+
822+
if (App.getCurrentScreen() === 'duel') render();
823+
});
824+
}
825+
759826
function _findRelatedDuel(state, opponent) {
760827
var user = VizAccount.getCurrentUser();
761828
var duel;

plan.md

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,47 @@
1-
# Viz-magic — chronicle posts-only cleanup plan
1+
# Viz-magic — duel opponent resolution + blessing UX plan
22

33
## Scope
4-
- Вернуть хронику к исходной модели: лента постов пользователей, а не mixed activity feed.
5-
- Убрать игровые события и bless/award narrative из основной ленты хроники.
6-
- Сохранить кнопку благословения как действие под постом.
4+
- Восстановить и зафиксировать корректный источник истины для участников дуэли во всех UI-переходах.
5+
- Убрать stale/local-state путаницу в duel screen и подтягивать реальные данные противника, когда они есть на chain/grimoire.
6+
- Проверить blessing flow на предмет реального level restriction и, если ограничения нет, сделать UX хроники понятным для низких уровней.
7+
- Сохранить все уже внесённые изменения по guild / inventory / chronicle feed cleanup.
78

89
## Non-goals
9-
- Не трогать VoiceProtocol и storage формат пользовательских постов.
10-
- Не проектировать отдельную activity-вкладку в этом проходе.
11-
- Не ломать compose/send flow и bless action под постом.
10+
- Не откатывать текущий `main`.
11+
- Не менять on-chain duel/blessing protocol без необходимости.
12+
- Не возвращать mixed activity feed в Chronicle.
1213

1314
## Milestones
14-
1. Зафиксировать источник истины для хроники: посты из voice/publication + локально отправленные post entries.
15-
2. Убрать сбор recentActions/duel history в основной feed.
16-
3. Оставить табы working поверх post-only набора.
17-
4. Прогнать локальные проверки JS.
15+
1. Проследить duel flow: `challenge -> accept -> state -> render pre-duel/active duel`.
16+
2. Привязать duel UI к canonical duel participants из chain state, а не к устаревшему локальному экранному state.
17+
3. Добавить безопасную догрузку character/grimoire данных для opponent, если локальный state их ещё не знает.
18+
4. Проверить blessing flow и явно отразить в UX, что blessing доступен через Chronicle posts других магов.
19+
5. Прогнать `node --check` для изменённых файлов, затем commit + push в `main`.
1820

1921
## Expected files or subsystems
22+
- `app/js/ui/screens/duel.js`
2023
- `app/js/ui/screens/chronicle.js`
24+
- `app/js/i18n/ru.js`
25+
- `app/js/i18n/en.js`
2126

2227
## Implementation tasks
23-
- Оставить в ленте только `chronicle_post` и voice/publication entries с required tag.
24-
- Не рендерить blessing/hunt/rest/duel как feed entries.
25-
- Сохранить локальный optimistic post injection после отправки пользователем.
28+
- В duel screen вычислять opponent из `duel.challenger/duel.target` при наличии state-дуэли.
29+
- При отсутствии локального character state для участника пробовать восстановить его из chain grimoire и перерендерить экран.
30+
- Убрать misleading blessing CTA там, где благословить нельзя (например, свой же пост).
31+
- Добавить понятное объяснение в Chronicle, когда в ленте нет чужих постов для blessing.
2632

2733
## Validation
28-
- В хронике больше не появляются bless/award/hunt/duel/rest записи.
29-
- Пользовательские посты по-прежнему видны.
30-
- Кнопка благословения под постом остаётся рабочей.
31-
- `node --check` проходит для изменённых JS файлов.
34+
- Duel pre-screen и active duel показывают корректного opponent и его реальные name/class/level, если данные есть на chain.
35+
- Экран дуэли больше не зависит от stale `opponent` fallback как от основного источника истины.
36+
- В коде нет level-based restriction для blessing.
37+
- Chronicle явно объясняет, что blessing работает через posts, и не предлагает bless на собственных записях.
38+
- `node --check` проходит для изменённых файлов.
3239

3340
## Risks or assumptions
34-
- Временный optimistic local post живёт в `recentActions`, но фильтруется только как `chronicle_post`.
35-
- Если потом понадобится activity feed, его лучше делать отдельным экраном/вкладкой, не в основной хронике.
41+
- Если у opponent нет grimoire на chain или node не вернёт account metadata, UI останется на account fallback, но это будет уже честный degradation path.
42+
- Chronicle по-прежнему зависит от наличия реальных posts; если никто не пишет, blessable targets не появятся.
3643

3744
## Definition of done
38-
- Хроника снова воспринимается как лента постов.
39-
- Игровые события больше не засоряют social feed.
45+
- Duel bug исправлен на уровне источника данных, а не замаскирован.
46+
- Blessing UX не создаёт ложное ощущение level cap и явно объясняет модель через Chronicle posts.
47+
- Изменения проверены синтаксически и отправлены в `main`.

0 commit comments

Comments
 (0)