Skip to content

Commit ae5ffac

Browse files
web3blindclaude
andcommitted
Add guild.listing action for guild discovery by new players
Officers can broadcast a guild.listing that includes the creation block. Fresh clients without checkpoints pick up these listings during the 24h sync window, fetch the creation block, and hydrate the full guild data so it appears in the recommended guilds list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 728c52f commit ae5ffac

File tree

6 files changed

+144
-3
lines changed

6 files changed

+144
-3
lines changed

app/js/config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ var VizMagicConfig = (function() {
166166
// Phase 5: Quests & Boss
167167
QUEST_ACCEPT: 'quest.accept',
168168
QUEST_COMPLETE: 'quest.complete',
169-
BOSS_ATTACK: 'boss.attack'
169+
BOSS_ATTACK: 'boss.attack',
170+
// Guild discovery
171+
GUILD_LISTING: 'guild.listing'
170172
};
171173

172174
/** Asset formatting helpers */

app/js/engine/state-engine.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ var StateEngine = (function() {
2727
social: {
2828
knownAccounts: []
2929
},
30+
guildListings: [], // [{guild_id, created_block, sender, blockNum}]
3031
// Phase 5: Living World
3132
worldBoss: null,
3233
quests: {}, // account → playerQuestState
@@ -285,6 +286,9 @@ var StateEngine = (function() {
285286
case AT.GUILD_PEACE:
286287
events = events.concat(_handleGuildPeace(sender, action.data, blockNum));
287288
break;
289+
case AT.GUILD_LISTING:
290+
events = events.concat(_handleGuildListing(sender, action.data, blockNum));
291+
break;
288292

289293
// --- Crafting ---
290294
case AT.CRAFT:
@@ -739,6 +743,32 @@ var StateEngine = (function() {
739743
return [{ type: 'guild_peace', warRef: data.war_ref, declarer: sender }];
740744
}
741745

746+
function _handleGuildListing(sender, data, blockNum) {
747+
if (!data.guild_id || !data.created_block) return [];
748+
// Verify sender is a member of the guild (if guild is in state)
749+
var guild = worldState.guilds[data.guild_id];
750+
if (guild && !guild.members[sender]) return [];
751+
752+
if (!worldState.guildListings) worldState.guildListings = [];
753+
// Deduplicate: keep only the latest listing per guild
754+
for (var i = worldState.guildListings.length - 1; i >= 0; i--) {
755+
if (worldState.guildListings[i].guild_id === data.guild_id) {
756+
worldState.guildListings.splice(i, 1);
757+
}
758+
}
759+
worldState.guildListings.push({
760+
guild_id: data.guild_id,
761+
created_block: data.created_block | 0,
762+
sender: sender,
763+
blockNum: blockNum
764+
});
765+
// Keep max 50 listings
766+
while (worldState.guildListings.length > 50) {
767+
worldState.guildListings.shift();
768+
}
769+
return [{ type: 'guild_listing', guildId: data.guild_id, sender: sender, createdBlock: data.created_block }];
770+
}
771+
742772
// ==========================================
743773
// Territory / Siege Action Handlers
744774
// ==========================================

app/js/i18n/en.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ var LangEN = {
411411
guild_invite_send: 'Send Invite',
412412
guild_invite_sent: 'Invite sent!',
413413
guild_invite_received: 'You received a guild invite. Opening the guild screen.',
414+
guild_listing: 'Promote Guild',
415+
guild_listing_sent: 'Guild promoted! It will be visible to other players.',
416+
guild_listing_hint: 'Promote your guild once a day so new players can discover it.',
414417
guild_pending_invites: 'Pending Invites',
415418
guild_accept_invite: 'Accept Invite',
416419
guild_invited_by: 'Invited by',

app/js/i18n/ru.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ var LangRU = {
411411
guild_invite_send: 'Отправить приглашение',
412412
guild_invite_sent: 'Приглашение отправлено!',
413413
guild_invite_received: 'Вас пригласили в гильдию. Открываю раздел гильдии.',
414+
guild_listing: 'Продвинуть гильдию',
415+
guild_listing_sent: 'Гильдия продвигается! Она будет видна другим игрокам.',
416+
guild_listing_hint: 'Продвигайте гильдию раз в сутки, чтобы она была видна новым игрокам.',
414417
guild_pending_invites: 'Ожидающие приглашения',
415418
guild_accept_invite: 'Принять приглашение',
416419
guild_invited_by: 'Пригласил',

app/js/protocols/guild-protocol.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,22 @@ var GuildProtocol = (function() {
181181
};
182182
}
183183

184+
/**
185+
* Create guild.listing action — advertise guild for discovery
186+
* @param {string} guildId
187+
* @param {number} createdBlock - block where guild.create was broadcast
188+
* @returns {Object}
189+
*/
190+
function createGuildListingAction(guildId, createdBlock) {
191+
return {
192+
t: AT.GUILD_LISTING,
193+
d: {
194+
guild_id: guildId,
195+
created_block: createdBlock | 0
196+
}
197+
};
198+
}
199+
184200
// --- Broadcast Helpers ---
185201

186202
/**
@@ -244,6 +260,14 @@ var GuildProtocol = (function() {
244260
broadcastGuildAction(action, callback);
245261
}
246262

263+
/**
264+
* Broadcast guild listing (advertise)
265+
*/
266+
function broadcastGuildListing(guildId, createdBlock, callback) {
267+
var action = createGuildListingAction(guildId, createdBlock);
268+
broadcastGuildAction(action, callback);
269+
}
270+
247271
/**
248272
* Broadcast peace
249273
*/
@@ -288,6 +312,7 @@ var GuildProtocol = (function() {
288312
createSiegeDeclareAction: createSiegeDeclareAction,
289313
createSiegeCommitAction: createSiegeCommitAction,
290314
createTerritoryClaimAction: createTerritoryClaimAction,
315+
createGuildListingAction: createGuildListingAction,
291316

292317
// Broadcast wrappers
293318
broadcastGuildAction: broadcastGuildAction,
@@ -300,6 +325,7 @@ var GuildProtocol = (function() {
300325
broadcastDeclarePeace: broadcastDeclarePeace,
301326
broadcastDeclareSiege: broadcastDeclareSiege,
302327
broadcastSiegeCommit: broadcastSiegeCommit,
303-
broadcastTerritoryClaim: broadcastTerritoryClaim
328+
broadcastTerritoryClaim: broadcastTerritoryClaim,
329+
broadcastGuildListing: broadcastGuildListing
304330
};
305331
})();

app/js/ui/screens/guild.js

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var GuildScreen = (function() {
88

99
var t = Helpers.t;
1010
var _seenGuildNotifications = {};
11+
var _listingFetchInProgress = {};
1112

1213
function render() {
1314
var container = Helpers.$('screen-guild');
@@ -184,6 +185,10 @@ var GuildScreen = (function() {
184185

185186
html += '<button class="btn btn-secondary guild-btn" id="btn-guild-patronage" aria-label="' + t('guild_patronage') + '">';
186187
html += '\uD83E\uDD1D ' + t('guild_patronage') + '</button>';
188+
189+
html += '<button class="btn btn-secondary guild-btn" id="btn-guild-listing" aria-label="' + t('guild_listing') + '">';
190+
html += '\uD83D\uDCE3 ' + t('guild_listing') + '</button>';
191+
html += '<p class="guild-listing-hint">' + t('guild_listing_hint') + '</p>';
187192
}
188193

189194
html += '<button class="btn btn-secondary guild-btn" id="btn-guild-treasury" aria-label="' + t('guild_treasury') + '">';
@@ -211,6 +216,8 @@ var GuildScreen = (function() {
211216
* Render no-guild state: recommended guilds + create button
212217
*/
213218
function _renderNoGuild(container, state, user) {
219+
// Kick off async hydration of guilds from listings
220+
_hydrateGuildListings(state);
214221
var html = '';
215222
var pendingInvites = _getPendingInvites(state, user);
216223
html += '<div class="guild-hall" role="region" aria-label="' + t('guild_title') + '">';
@@ -293,6 +300,59 @@ var GuildScreen = (function() {
293300
return invites;
294301
}
295302

303+
/**
304+
* Hydrate guilds from listings — fetch creation blocks for unknown guilds.
305+
* Runs asynchronously; re-renders guild screen when new data arrives.
306+
*/
307+
function _hydrateGuildListings(state) {
308+
var listings = state.guildListings;
309+
if (!listings || listings.length === 0) return;
310+
for (var i = 0; i < listings.length; i++) {
311+
var entry = listings[i];
312+
var gid = entry.guild_id;
313+
var createdBlock = entry.created_block;
314+
if (!gid || !createdBlock) continue;
315+
// Skip if guild already in state and not a placeholder
316+
if (state.guilds[gid] && !state.guilds[gid].isPlaceholder) continue;
317+
// Skip if already fetching
318+
if (_listingFetchInProgress[gid]) continue;
319+
_listingFetchInProgress[gid] = true;
320+
(function(guildId, block) {
321+
viz.api.getBlock(block, function(err, blockData) {
322+
delete _listingFetchInProgress[guildId];
323+
if (err || !blockData || !blockData.transactions) return;
324+
// Find guild.create for this guild_id in the block
325+
for (var ti = 0; ti < blockData.transactions.length; ti++) {
326+
var tx = blockData.transactions[ti];
327+
if (!tx.operations) continue;
328+
for (var oi = 0; oi < tx.operations.length; oi++) {
329+
var op = tx.operations[oi];
330+
if (op[0] !== 'custom' || op[1].id !== 'VM') continue;
331+
try {
332+
var parsed = JSON.parse(op[1].json);
333+
if (parsed.t !== 'guild.create') continue;
334+
if (parsed.d && parsed.d.id === guildId) {
335+
var sender = (op[1].required_regular_auths && op[1].required_regular_auths[0]) || '';
336+
var currentState = StateEngine.getState();
337+
var guild = GuildSystem.createGuild(guildId, sender, parsed.d, block);
338+
if (guild) {
339+
currentState.guilds[guildId] = guild;
340+
// Re-render if guild screen is active
341+
var container = Helpers.$('screen-guild');
342+
if (container && !container.getAttribute('aria-hidden')) {
343+
render();
344+
}
345+
}
346+
return;
347+
}
348+
} catch (e) {}
349+
}
350+
}
351+
});
352+
})(gid, createdBlock);
353+
}
354+
}
355+
296356
/**
297357
* Get list of all guilds for display
298358
*/
@@ -382,6 +442,23 @@ var GuildScreen = (function() {
382442
});
383443
}
384444

445+
// Listing (advertise) button
446+
var listingBtn = container.querySelector('#btn-guild-listing');
447+
if (listingBtn) {
448+
listingBtn.addEventListener('click', function() {
449+
listingBtn.disabled = true;
450+
GuildProtocol.broadcastGuildListing(guild.id, guild.createdBlock || 0, function(err) {
451+
listingBtn.disabled = false;
452+
if (err) {
453+
Toast.error(t('error_network'));
454+
} else {
455+
Toast.success(t('guild_listing_sent'));
456+
SoundManager.play('tap');
457+
}
458+
});
459+
});
460+
}
461+
385462
var settingsBtn = container.querySelector('#btn-guild-settings');
386463
if (settingsBtn) {
387464
settingsBtn.addEventListener('click', function() {
@@ -766,7 +843,7 @@ var GuildScreen = (function() {
766843
* Called once when the screen module loads.
767844
*/
768845
function init() {
769-
var events = ['guild_created', 'guild_joined', 'guild_left', 'guild_promoted', 'guild_invite'];
846+
var events = ['guild_created', 'guild_joined', 'guild_left', 'guild_promoted', 'guild_invite', 'guild_listing'];
770847
for (var i = 0; i < events.length; i++) {
771848
Helpers.EventBus.on(events[i], function() {
772849
// Re-render only if guild screen is currently visible

0 commit comments

Comments
 (0)