Skip to content

Commit bfa5341

Browse files
author
web3blind
committed
Route quest UI actions through blockchain
1 parent cf108c2 commit bfa5341

9 files changed

Lines changed: 230 additions & 29 deletions

File tree

app/index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
<script src="lib/viz.min.js"></script>
112112

113113
<!-- Config -->
114-
<script src="js/config.js?v=20260621d"></script>
114+
<script src="js/config.js?v=20260621m"></script>
115115

116116
<!-- I18n -->
117117
<script src="js/i18n/ru.js?v=20260621d"></script>
@@ -126,7 +126,7 @@
126126
<script src="js/blockchain/connection.js"></script>
127127
<script src="js/blockchain/account.js"></script>
128128
<script src="js/blockchain/history-source.js?v=20260621d"></script>
129-
<script src="js/blockchain/broadcast.js"></script>
129+
<script src="js/blockchain/broadcast.js?v=20260621m"></script>
130130
<script src="js/blockchain/invite.js"></script>
131131

132132
<!-- Protocols -->
@@ -164,7 +164,7 @@
164164
<script src="js/engine/quest-system.js?v=20260621e"></script>
165165
<script src="js/engine/loci.js"></script>
166166
<script src="js/engine/npc-framework.js"></script>
167-
<script src="js/engine/state-engine.js?v=20260621l"></script>
167+
<script src="js/engine/state-engine.js?v=20260621m"></script>
168168
<script src="js/engine/daily-leaderboard-storage.js"></script>
169169
<script src="js/engine/daily-leaderboard.js"></script>
170170

@@ -191,7 +191,7 @@
191191
<script src="js/ui/screens/map.js?v=20260621h"></script>
192192
<script src="js/ui/screens/marketplace.js?v=20260621k"></script>
193193
<script src="js/ui/screens/crafting.js?v=20260621j"></script>
194-
<script src="js/ui/screens/quests.js?v=20260621e"></script>
194+
<script src="js/ui/screens/quests.js?v=20260621m"></script>
195195
<script src="js/ui/screens/world-boss.js"></script>
196196
<script src="js/ui/screens/settings.js"></script>
197197
<script src="js/ui/screens/help.js"></script>

app/js/blockchain/broadcast.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,19 @@ var VizBroadcast = (function() {
206206
});
207207
}
208208

209+
/**
210+
* Broadcast quest action (accept / complete / abandon) as a VM custom op.
211+
* @param {string} type
212+
* @param {Object} data
213+
* @param {Function} callback
214+
*/
215+
function questAction(type, data, callback) {
216+
gameAction({
217+
t: type,
218+
d: data || {}
219+
}, callback);
220+
}
221+
209222
/**
210223
* Update account metadata (Grimoire)
211224
* @param {string} jsonMetadata - JSON string
@@ -294,6 +307,7 @@ var VizBroadcast = (function() {
294307
gameAction: gameAction,
295308
huntAction: huntAction,
296309
armageddonAction: armageddonAction,
310+
questAction: questAction,
297311
updateMetadata: updateMetadata,
298312
chroniclePost: chroniclePost,
299313
delegateShares: delegateShares,

app/js/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ var VizMagicConfig = (function() {
181181
// Phase 5: Quests & Boss
182182
QUEST_ACCEPT: 'quest.accept',
183183
QUEST_COMPLETE: 'quest.complete',
184+
QUEST_ABANDON: 'quest.abandon',
184185
BOSS_ATTACK: 'boss.attack',
185186
// Guild discovery
186187
GUILD_LISTING: 'guild.listing'

app/js/engine/state-engine.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ var StateEngine = (function() {
355355
case 'quest.complete':
356356
events = events.concat(_handleQuestComplete(sender, action.data, blockNum));
357357
break;
358+
case 'quest.abandon':
359+
events = events.concat(_handleQuestAbandon(sender, action.data, blockNum));
360+
break;
358361

359362
// --- Phase 5: Boss attack ---
360363
case 'boss.attack':
@@ -1070,11 +1073,16 @@ var StateEngine = (function() {
10701073
_ensureQuests(sender);
10711074
var character = worldState.characters[sender];
10721075
if (!character) return [];
1073-
var questData = GameQuests.getQuest(data.quest_id);
1076+
var questData;
1077+
if (data && data.daily) {
1078+
questData = QuestSystem.generateDailyProphecy(data.daily_block || blockNum, character.level);
1079+
} else {
1080+
questData = GameQuests.getQuest(data.quest_id);
1081+
}
10741082
if (!questData) return [];
10751083
var result = QuestSystem.acceptQuest(questData, character, worldState.quests[sender], blockNum);
10761084
if (!result.success) return [];
1077-
return [{ type: 'quest_accepted', account: sender, questId: data.quest_id, blockNum: blockNum }];
1085+
return [{ type: 'quest_accepted', account: sender, questId: questData.id, daily: !!(data && data.daily), blockNum: blockNum }];
10781086
}
10791087

10801088
function _handleQuestComplete(sender, data, blockNum) {
@@ -1088,6 +1096,14 @@ var StateEngine = (function() {
10881096
return [{ type: 'quest_completed', account: sender, questId: data.quest_id, rewards: result.rewards, blockNum: blockNum }];
10891097
}
10901098

1099+
function _handleQuestAbandon(sender, data, blockNum) {
1100+
if (typeof QuestSystem === 'undefined') return [];
1101+
_ensureQuests(sender);
1102+
var ok = QuestSystem.abandonQuest(data.quest_id, worldState.quests[sender]);
1103+
if (!ok) return [];
1104+
return [{ type: 'quest_abandoned', account: sender, questId: data.quest_id, blockNum: blockNum }];
1105+
}
1106+
10911107
function _handleBossAttack(sender, data, blockNum, blockHash) {
10921108
if (typeof WorldBoss === 'undefined') return [];
10931109

@@ -1316,6 +1332,25 @@ var StateEngine = (function() {
13161332
return events[0];
13171333
}
13181334

1335+
function processQuestAcceptResult(account, questId, daily, dailyBlock, blockNum) {
1336+
var data = { quest_id: questId || '', daily: !!daily, daily_block: dailyBlock || 0 };
1337+
var events = _handleQuestAccept(account, data, blockNum || 0);
1338+
if (!events.length) return null;
1339+
return events[0];
1340+
}
1341+
1342+
function processQuestCompleteResult(account, questId, blockNum) {
1343+
var events = _handleQuestComplete(account, { quest_id: questId }, blockNum || 0);
1344+
if (!events.length) return null;
1345+
return events[0];
1346+
}
1347+
1348+
function processQuestAbandonResult(account, questId, blockNum) {
1349+
var events = _handleQuestAbandon(account, { quest_id: questId }, blockNum || 0);
1350+
if (!events.length) return null;
1351+
return events[0];
1352+
}
1353+
13191354
/**
13201355
* Process Armageddon for live UI — same logic as _handleHuntArmageddon.
13211356
* @param {string} account
@@ -1356,6 +1391,9 @@ var StateEngine = (function() {
13561391
processMarketListResult: processMarketListResult,
13571392
processMarketCancelResult: processMarketCancelResult,
13581393
processMarketBuyResult: processMarketBuyResult,
1394+
processQuestAcceptResult: processQuestAcceptResult,
1395+
processQuestCompleteResult: processQuestCompleteResult,
1396+
processQuestAbandonResult: processQuestAbandonResult,
13591397
processArmageddonResult: processArmageddonResult,
13601398
reset: reset
13611399
};

app/js/ui/screens/quests.js

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -286,14 +286,7 @@ var QuestsScreen = (function() {
286286
}
287287

288288
if (questData) {
289-
var result = QuestSystem.acceptQuest(questData, character, playerQuests, blockNum);
290-
if (result.success) {
291-
SoundManager.play('quest_accept');
292-
Toast.success(Helpers.t('quest_accepted'));
293-
render();
294-
} else {
295-
Toast.error(Helpers.t('quest_error_' + result.error));
296-
}
289+
_broadcastQuestAction('accept', questId, isDaily, blockNum);
297290
}
298291
});
299292
}
@@ -303,14 +296,7 @@ var QuestsScreen = (function() {
303296
for (var j = 0; j < completeBtns.length; j++) {
304297
completeBtns[j].addEventListener('click', function() {
305298
var questId = this.getAttribute('data-quest-id');
306-
var user = VizAccount.getCurrentUser();
307-
var inventory = StateEngine.getInventory(user);
308-
var result = QuestSystem.completeQuest(questId, playerQuests, character, inventory, blockNum);
309-
if (result.success) {
310-
SoundManager.play('quest_complete');
311-
Toast.success(Helpers.t('quest_completed') + ' +' + result.rewards.xp + ' XP');
312-
render();
313-
}
299+
_broadcastQuestAction('complete', questId, false, blockNum);
314300
});
315301
}
316302

@@ -319,13 +305,61 @@ var QuestsScreen = (function() {
319305
for (var k = 0; k < abandonBtns.length; k++) {
320306
abandonBtns[k].addEventListener('click', function() {
321307
var questId = this.getAttribute('data-quest-id');
322-
QuestSystem.abandonQuest(questId, playerQuests);
323-
Toast.info(Helpers.t('quest_abandoned'));
324-
render();
308+
_broadcastQuestAction('abandon', questId, false, blockNum);
325309
});
326310
}
327311
}
328312

313+
function _broadcastQuestAction(kind, questId, isDaily, dailyBlock) {
314+
var cfg = VizMagicConfig;
315+
var type = cfg.ACTION_TYPES.QUEST_ACCEPT;
316+
if (kind === 'complete') type = cfg.ACTION_TYPES.QUEST_COMPLETE;
317+
if (kind === 'abandon') type = cfg.ACTION_TYPES.QUEST_ABANDON;
318+
var data = { quest_id: questId || '' };
319+
if (isDaily) {
320+
data.daily = true;
321+
data.daily_block = dailyBlock || 0;
322+
}
323+
324+
VizBroadcast.questAction(type, data, function(err, result) {
325+
if (err) {
326+
Toast.error(Helpers.t('error_network'));
327+
return;
328+
}
329+
330+
var user = VizAccount.getCurrentUser();
331+
var finalBlockNum = (result && result.block_num) ||
332+
(result && result.action && result.action.block_num) ||
333+
(StateEngine.getState().headBlock || 0);
334+
var event = null;
335+
if (kind === 'accept') {
336+
event = StateEngine.processQuestAcceptResult(user, questId, isDaily, dailyBlock, finalBlockNum);
337+
} else if (kind === 'complete') {
338+
event = StateEngine.processQuestCompleteResult(user, questId, finalBlockNum);
339+
} else if (kind === 'abandon') {
340+
event = StateEngine.processQuestAbandonResult(user, questId, finalBlockNum);
341+
}
342+
343+
if (!event) {
344+
Toast.error(Helpers.t('error_network'));
345+
return;
346+
}
347+
348+
StateEngine.getState().headBlock = finalBlockNum;
349+
CheckpointSystem.saveCheckpoint('global', finalBlockNum, StateEngine.getState(), function() {});
350+
if (kind === 'complete') {
351+
SoundManager.play('quest_complete');
352+
Toast.success(Helpers.t('quest_completed'));
353+
} else if (kind === 'abandon') {
354+
Toast.info(Helpers.t('quest_abandoned'));
355+
} else {
356+
SoundManager.play('quest_accept');
357+
Toast.success(Helpers.t('quest_accepted'));
358+
}
359+
render();
360+
});
361+
}
362+
329363
function _describeObjective(obj, t) {
330364
if (!obj) return '';
331365
if (obj.type === 'explore') return t('quest_obj_explore_detail', { count: obj.required });

app/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Viz Magic — Service Worker
2-
var CACHE_NAME = 'viz-magic-v25';
2+
var CACHE_NAME = 'viz-magic-v26';
33
var ASSETS = [
44
'/',
55
'/index.html',

tests/core-screen-accessibility-smoke.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ async function run() {
9090
var badLogs = logs.filter(function(l) { return l.type === 'error' || l.type === 'pageerror'; });
9191
assert.deepStrictEqual(failed, [], 'all core screens should render with named controls and no raw keys');
9292
assert.deepStrictEqual(badLogs, [], 'core screen smoke should not emit console errors/page errors');
93-
assert.ok(result.cachesKeys.indexOf('viz-magic-v25') !== -1, 'service worker cache should use latest cache name');
93+
assert.ok(result.cachesKeys.indexOf('viz-magic-v26') !== -1, 'service worker cache should use latest cache name');
9494
assert.ok(result.scripts.some(function(src) { return src.indexOf('helpers.js?v=20260621l') !== -1; }), 'helpers script should be cache-busted');
9595
assert.ok(result.scripts.some(function(src) { return src.indexOf('territory.js?v=20260621l') !== -1; }), 'territory script should be cache-busted');
96-
assert.ok(result.scripts.some(function(src) { return src.indexOf('state-engine.js?v=20260621l') !== -1; }), 'state engine script should be cache-busted');
96+
assert.ok(result.scripts.some(function(src) { return src.indexOf('state-engine.js?v=20260621m') !== -1; }), 'state engine script should be cache-busted');
9797
console.log('PASS core screen accessibility smoke');
9898
} finally {
9999
if (browser) await browser.close();

tests/player-bug-regressions.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ test('high-traffic UI narration and inventory stat labels are translated', funct
327327

328328
test('service worker updates quickly and keeps navigations network-first', function () {
329329
const swJs = read('app/sw.js');
330-
assert.ok(/viz-magic-v25/.test(swJs), 'service worker cache version should be bumped');
330+
assert.ok(/viz-magic-v26/.test(swJs), 'service worker cache version should be bumped');
331331
assert.ok(/self\.skipWaiting\(\)/.test(swJs), 'service worker should activate new cache without waiting for all tabs to close');
332332
assert.ok(/self\.clients\.claim\(\)/.test(swJs), 'service worker should claim clients after activation');
333333
assert.ok(/event\.request\.mode === 'navigate'[\s\S]*fetch\(event\.request\)/.test(swJs), 'navigation requests should prefer network to avoid stale cached index');
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var http = require('http');
5+
var fs = require('fs');
6+
var path = require('path');
7+
var puppeteer = require('puppeteer');
8+
9+
var APP_DIR = path.join(__dirname, '..', 'app');
10+
var APP_PORT = 8215;
11+
var MIME = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml' };
12+
13+
function startAppServer() {
14+
return new Promise(function(resolve) {
15+
var srv = http.createServer(function(req, res) {
16+
var url = req.url.split('?')[0];
17+
if (url === '/') url = '/index.html';
18+
var filePath = path.join(APP_DIR, url);
19+
fs.readFile(filePath, function(err, data) {
20+
if (err) { res.writeHead(404); res.end('Not found'); return; }
21+
res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath)] || 'application/octet-stream' });
22+
res.end(data);
23+
});
24+
});
25+
srv.listen(APP_PORT, '127.0.0.1', function() { resolve(srv); });
26+
});
27+
}
28+
29+
async function run() {
30+
var server = await startAppServer();
31+
var browser;
32+
try {
33+
browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });
34+
var page = await browser.newPage();
35+
await page.goto('http://127.0.0.1:' + APP_PORT + '/?cachebust=quest-browser-transaction-smoke', { waitUntil: 'networkidle0', timeout: 15000 });
36+
37+
var result = await page.evaluate(async function() {
38+
localStorage.setItem('viz_magic_session', JSON.stringify({ currentUser: 'quest-smoke', users: { 'quest-smoke': { regular_key: 'fixture-key' } } }));
39+
VizAccount.init();
40+
StateEngine.reset();
41+
var state = StateEngine.getState();
42+
state.headBlock = 12000;
43+
state.characters['quest-smoke'] = {
44+
name: 'Quest Smoke', className: 'embercaster', level: 12, xp: 1000,
45+
hp: 100, maxHp: 100, mana: 10000, currentZone: 'commons_first_light',
46+
pot: 8, res: 5, swf: 4, int: 10, for_: 4, spells: ['firebolt']
47+
};
48+
state.inventories['quest-smoke'] = [];
49+
state.quests['quest-smoke'] = QuestSystem.createPlayerQuestState();
50+
51+
var calls = [];
52+
VizBroadcast.questAction = function(type, data, callback) {
53+
calls.push({ type: type, data: JSON.parse(JSON.stringify(data || {})) });
54+
callback(null, { block_num: 12010 });
55+
};
56+
57+
App.navigateTo('quests');
58+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
59+
document.querySelector('[data-tab="available"]').click();
60+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
61+
var acceptBtn = document.querySelector('.quest-accept-btn');
62+
if (!acceptBtn) return { error: 'accept button missing', calls: calls };
63+
var questId = acceptBtn.getAttribute('data-quest-id');
64+
acceptBtn.click();
65+
await new Promise(function(resolve) { setTimeout(resolve, 120); });
66+
var activeAfterAccept = StateEngine.getState().quests['quest-smoke'].active.map(function(q) { return q.id; });
67+
document.querySelector('[data-tab="active"]').click();
68+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
69+
70+
var abandonBtn = document.querySelector('.quest-abandon-btn');
71+
if (!abandonBtn) return { error: 'abandon button missing after accept', calls: calls, activeAfterAccept: activeAfterAccept };
72+
abandonBtn.click();
73+
await new Promise(function(resolve) { setTimeout(resolve, 120); });
74+
var activeAfterAbandon = StateEngine.getState().quests['quest-smoke'].active.map(function(q) { return q.id; });
75+
76+
document.querySelector('[data-tab="daily"]').click();
77+
await new Promise(function(resolve) { setTimeout(resolve, 80); });
78+
var dailyBtn = document.querySelector('.quest-accept-btn[data-daily="true"]');
79+
if (!dailyBtn) return { error: 'daily accept button missing', calls: calls, activeAfterAccept: activeAfterAccept, activeAfterAbandon: activeAfterAbandon };
80+
dailyBtn.click();
81+
await new Promise(function(resolve) { setTimeout(resolve, 120); });
82+
var activeAfterDaily = StateEngine.getState().quests['quest-smoke'].active.map(function(q) { return q.id; });
83+
84+
return {
85+
calls: calls,
86+
questId: questId,
87+
activeAfterAccept: activeAfterAccept,
88+
activeAfterAbandon: activeAfterAbandon,
89+
activeAfterDaily: activeAfterDaily,
90+
headBlock: StateEngine.getState().headBlock
91+
};
92+
});
93+
94+
assert.ifError(result.error);
95+
assert.strictEqual(result.calls[0].type, 'quest.accept', 'available quest accept should broadcast VM action');
96+
assert.strictEqual(result.calls[0].data.quest_id, result.questId, 'quest id should be sent on chain');
97+
assert.ok(result.activeAfterAccept.indexOf(result.questId) !== -1, 'accepted quest should update StateEngine after broadcast');
98+
assert.strictEqual(result.calls[1].type, 'quest.abandon', 'abandon should broadcast VM action');
99+
assert.deepStrictEqual(result.activeAfterAbandon, [], 'abandoned quest should update StateEngine after broadcast');
100+
assert.strictEqual(result.calls[2].type, 'quest.accept', 'daily quest accept should broadcast VM action');
101+
assert.strictEqual(result.calls[2].data.daily, true, 'daily quest should include deterministic daily marker');
102+
assert.ok(result.activeAfterDaily.length === 1 && /^daily_/.test(result.activeAfterDaily[0]), 'daily quest should be accepted through StateEngine');
103+
assert.strictEqual(result.headBlock, 12010, 'local checkpoint head should advance to broadcast block');
104+
console.log('PASS quest browser transaction smoke');
105+
} finally {
106+
if (browser) await browser.close();
107+
await new Promise(function(resolve) { server.close(resolve); });
108+
}
109+
}
110+
111+
run().catch(function(err) {
112+
console.error('FAIL quest browser transaction smoke: ' + (err && err.stack || err));
113+
process.exit(1);
114+
});

0 commit comments

Comments
 (0)