摆摊系统
摆摊系统完全指南 | 开发进度追踪
摆摊系统是《蛙噻!好摊》的核心玩法之一。作为一名取经路上的行者,你需要通过低买高卖来积攒盘缠。掌握市场规律、把握交易时机、与各路商人周旋,是成为一代商业传奇的必经之路。
摆摊流程
完整的摆摊经营流程包含以下六个步骤:
进货采购
在各地商人处或副本中购买商品。不同地区商品价格存在差异,是套利的基础。
定价策略
根据市场行情设定售价。定价过高滞销,过低利润微薄。
选择摊位
选择摆摊的位置。城镇中心人流量大但竞争激烈。
摆摊销售
开始摆摊,NPC顾客陆续前来询价。可随时调整价格或下架商品。
议价成交
与顾客进行价格谈判。成功议价可获得额外利润。
收摊结算
收摊后结算已售出商品的收益。未售出商品返还背包。
商品类型
| 类型 | 特点 | 例子 | 风险 |
|---|---|---|---|
| 普通商品 | 供应充足,价格波动小 | 粮食、布匹、日常用品 | 低 |
| 稀有商品 | 供应有限,利润空间高 | 珍贵药材、精良装备 | 中等 |
| 特产商品 | 特定地区独有,跨区域贸易利润极高 | 花果山仙桃、火焰山火晶 | 高 |
摊位管理
四条街道
| 街道 | 主营商品 | 特点 | 人流量 |
|---|---|---|---|
| 东街 | 工具 | 工匠和冒险者的聚集地 | 中等 |
| 南街 | 综合商品 | 最大的综合市场 | 最高 |
| 西街 | 道具 | 炼金师和工匠的必去之地 | 中等 |
| 北街 | 装备 | 战士和冒险者的天堂 | 较高 |
摊位位置价值
| 位置类型 | 人流量 | 出售效率 | 适合阶段 |
|---|---|---|---|
| 中心区域 | 5星 | +50% | 中后期 |
| 街道中段 | 4星 | +25% | 中期 |
| 街道边缘 | 3星 | 标准 | 新手期 |
摊位自动前移机制
当某个摊位收摊后,该摊位后方的所有摊位会自动向前移动填补空位。只要你持续经营的时间够久,你的摊位会慢慢移动到更靠前的位置。
出摊费用
| 项目 | 说明 | 费率 |
|---|---|---|
| 出摊手续费 | 上架商品时需支付的手续费 | 商品市场价格的 1% |
| 交易税费 | 商品成功卖出时收取的税费 | 成交金额的 5% |
装饰系统
| 称号 | 解锁装饰 | 成交率加成 | 离线收益加成 |
|---|---|---|---|
| 小商贩 | 草创期6个 | +1% | - |
| 行商 | 立业期6个 | +2% | +1% |
| 富商 | 兴业期6个 | +3% | +2% |
| 巨贾 | 旺业期6个 | +4% | +3% |
| 商业大亨 | 鸿业期6个 | +5% | +4% |
| 大富翁 | 传世期6个 | +6% | +5% |
摆摊系统开发流程
按照以下计划逐步完成开发,完成所有计划即开发成功。
第一阶段:摊位管理模块
-
任务1.1:创建摊位数据结构
定义摊位类和街道数据
class Stall { constructor(id, street, index) { this.id = id; // 摊位ID this.street = street; // 所属街道:east/south/west/north this.index = index; // 在街道中的位置序号 this.owner = null; // 拥有者ID this.status = 'vacant'; // vacant/occupied/locked this.listedItems = []; // 上架商品列表 this.decorations = []; // 装饰列表 } } // 街道配置 const STREETS = { east: { name: '东街', specialty: 'tool', stallCount: 200 }, south: { name: '南街', specialty: 'misc', stallCount: 200 }, west: { name: '西街', specialty: 'potion', stallCount: 200 }, north: { name: '北街', specialty: 'equipment', stallCount: 200 } }; -
任务1.2:实现摊位管理类 StallManager
管理摊位的选择、占用、撤离
class StallManager { constructor() { this.stalls = []; this.initStalls(); } initStalls() { let id = 1; for (let street of Object.keys(STREETS)) { for (let i = 0; i < STREETS[street].stallCount; i++) { this.stalls.push(new Stall(`stall_${id++}`, street, i)); } } } // 获取空闲摊位 getVacantStalls(street) { return this.stalls.filter(s => s.street === street && s.status === 'vacant'); } // 占用摊位 occupyStall(stallId, playerId) { const stall = this.stalls.find(s => s.id === stallId); if (stall && stall.status === 'vacant') { stall.status = 'occupied'; stall.owner = playerId; return true; } return false; } // 撤离摊位 vacateStall(stallId) { const stall = this.stalls.find(s => s.id === stallId); if (stall) { stall.status = 'vacant'; stall.owner = null; stall.listedItems = []; return true; } return false; } } -
任务1.3:实现摊位自动对齐
收摊后后方摊位自动前移
class StallManager { // ... 其他方法 // 摊位自动前移对齐 autoAlignStalls(street) { const streetStalls = this.stalls .filter(s => s.street === street) .sort((a, b) => a.index - b.index); let currentIndex = 0; streetStalls.forEach(stall => { if (stall.status === 'occupied') { stall.index = currentIndex++; } }); } // 收摊时调用 vacateAndAlign(stallId) { const stall = this.stalls.find(s => s.id === stallId); if (!stall) return false; const street = stall.street; const result = this.vacateStall(stallId); if (result) { this.autoAlignStalls(street); } return result; } }
验证方法
创建摊位管理器,测试占用和撤离功能,验证自动对齐是否正确执行。
第二阶段:商品交易模块
-
任务2.1:定义商品数据结构
商品类和商品类型配置
class Item { constructor(id, name, type, rarity, basePrice) { this.id = id; this.name = name; this.type = type; // tool/armor/consumable/material this.rarity = rarity; // common/rare/epic/legendary this.basePrice = basePrice; this.currentPrice = basePrice; } } // 商品类型配置 const ITEM_TYPES = { tool: { name: '工具', streetBonus: { east: 1.2 } }, armor: { name: '防具', streetBonus: { north: 1.2 } }, consumable: { name: '消耗品', streetBonus: { west: 1.2 } }, material: { name: '材料', streetBonus: {} } }; // 上架商品记录 class ListedItem { constructor(item, price, quantity) { this.itemId = item.id; this.item = item; this.price = price; // 玩家设定的售价 this.quantity = quantity; this.listedAt = Date.now(); this.soldAt = null; } } -
任务2.2:实现交易系统类 TradingSystem
商品上架、下架、成交
class TradingSystem { constructor() { this.listings = new Map(); // stallId -> ListedItem[] } // 上架商品 listItem(stallId, item, price, quantity, player) { // 检查玩家库存 if (!player.hasItem(item.id, quantity)) { return { success: false, message: '商品数量不足' }; } // 计算手续费 const fee = Math.floor(item.basePrice * 0.01 * quantity); if (player.money < fee) { return { success: false, message: '余额不足支付手续费' }; } // 扣除商品和手续费 player.removeItem(item.id, quantity); player.money -= fee; // 添加上架记录 const listing = new ListedItem(item, price, quantity); if (!this.listings.has(stallId)) { this.listings.set(stallId, []); } this.listings.get(stallId).push(listing); return { success: true, fee: fee, listing: listing }; } // 下架商品 delistItem(stallId, listingIndex, player) { const listings = this.listings.get(stallId); if (!listings || !listings[listingIndex]) { return { success: false, message: '商品不存在' }; } const listing = listings.splice(listingIndex, 1)[0]; player.addItem(listing.item, listing.quantity); return { success: true, item: listing }; } } -
任务2.3:实现交易成交逻辑
NPC购买商品和收益结算
class TradingSystem { // ... 其他方法 // NPC购买商品 sellItem(stallId, listingIndex, quantity) { const listings = this.listings.get(stallId); if (!listings || !listings[listingIndex]) { return { success: false, message: '商品不存在' }; } const listing = listings[listingIndex]; const sellQty = Math.min(quantity, listing.quantity); // 计算收益(扣除5%税费) const grossEarnings = listing.price * sellQty; const tax = Math.floor(grossEarnings * 0.05); const netEarnings = grossEarnings - tax; // 更新上架数量 listing.quantity -= sellQty; if (listing.quantity <= 0) { listings.splice(listingIndex, 1); } return { success: true, quantity: sellQty, grossEarnings: grossEarnings, tax: tax, netEarnings: netEarnings }; } // 获取摊位总收益 getTotalEarnings(stallId) { const listings = this.listings.get(stallId) || []; let total = 0; listings.forEach(l => { if (l.soldAt) { total += l.netEarnings || 0; } }); return total; } }
验证方法
测试商品上架、下架、NPC购买流程,验证手续费和税费计算正确。
第三阶段:价格机制模块
-
任务3.1:实现市场价格计算
根据多种因素计算商品市场价
class PriceSystem { constructor() { this.basePrices = new Map(); // itemId -> basePrice this.priceModifiers = new Map(); // itemId -> modifier } // 计算市场价格 calculateMarketPrice(itemId, location, gameTime) { const basePrice = this.basePrices.get(itemId) || 100; let price = basePrice; // 位置加成 const locationBonus = this.getLocationBonus(location); price *= locationBonus; // 时段加成 const timeBonus = this.getTimeBonus(gameTime); price *= timeBonus; // 季节加成 const seasonBonus = this.getSeasonBonus(gameTime, itemId); price *= seasonBonus; // 供需波动 const demandModifier = this.priceModifiers.get(itemId) || 1.0; price *= demandModifier; return Math.floor(price); } getLocationBonus(location) { const bonuses = { center: 1.2, middle: 1.0, edge: 0.9 }; return bonuses[location] || 1.0; } getTimeBonus(gameTime) { const hour = gameTime.hour; if (hour >= 8 && hour < 12) return 1.1; // 上午高峰 if (hour >= 17 && hour < 20) return 1.15; // 傍晚高峰 if (hour >= 23 || hour < 5) return 0.8; // 深夜 return 1.0; } getSeasonBonus(gameTime, itemId) { // 根据季节和商品类型返回加成 return 1.0; } } -
任务3.2:实现定价反馈系统
根据定价与市场价对比给出反馈
class PriceSystem { // ... 其他方法 // 获取定价反馈 getPriceFeedback(listedPrice, marketPrice) { const ratio = listedPrice / marketPrice; if (ratio < 0.8) { return { type: 'very_low', message: '定价过低,商品快速售出', sellProbability: 0.95, avgSellTime: 15 * 60 * 1000 // 15分钟 }; } else if (ratio < 0.95) { return { type: 'low', message: '定价略低,成交较快', sellProbability: 0.8, avgSellTime: 30 * 60 * 1000 // 30分钟 }; } else if (ratio <= 1.1) { return { type: 'fair', message: '定价合理,正常成交', sellProbability: 0.6, avgSellTime: 2 * 60 * 60 * 1000 // 2小时 }; } else if (ratio <= 1.3) { return { type: 'high', message: '定价偏高,NPC议价中', sellProbability: 0.3, avgSellTime: 5 * 60 * 60 * 1000, // 5小时 bargainChance: 0.7 }; } else { return { type: 'very_high', message: '定价过高,无人问津', sellProbability: 0.05, avgSellTime: 10 * 60 * 60 * 1000, // 10小时 bargainChance: 0.3 }; } } } -
任务3.3:实现议价系统
NPC议价和玩家回应
class BargainingSystem { // NPC发起议价 initiateBargain(listing, marketPrice) { const ratio = listing.price / marketPrice; // 议价幅度(定价越高,议价幅度越大) const bargainRate = Math.min(0.3, (ratio - 1) * 0.5); const bargainPrice = Math.floor(listing.price * (1 - bargainRate)); return { npcId: this.generateNpcId(), originalPrice: listing.price, bargainPrice: bargainPrice, deadline: Date.now() + 5 * 60 * 1000, // 5分钟内回应 message: this.generateBargainMessage(bargainRate) }; } // 玩家接受议价 acceptBargain(bargain, listing) { listing.price = bargain.bargainPrice; return { success: true, finalPrice: bargain.bargainPrice }; } // 玩家拒绝议价 rejectBargain(bargain, listing) { // 降低成交概率 return { success: true, sellProbabilityModifier: 0.5, message: 'NPC离开,成交概率降低' }; } generateBargainMessage(rate) { if (rate < 0.1) return '老板,能便宜一点点吗?'; if (rate < 0.2) return '这价格有点贵啊,少点呗?'; return '老板,这也太贵了,便宜点我就买了!'; } }
验证方法
测试不同定价策略的反馈,验证议价系统是否正常工作。
第四阶段:装饰系统模块
-
任务4.1:定义装饰数据结构
装饰类型和效果配置
const DECORATION_TIERS = { startup: { name: '草创期', count: 6 }, growth: { name: '立业期', count: 6 }, develop: { name: '兴业期', count: 6 }, prosper: { name: '旺业期', count: 6 }, flourish: { name: '鸿业期', count: 6 }, legacy: { name: '传世期', count: 6 } }; const DECORATION_TYPES = { signboard: { name: '招牌类', effect: 'dealRate' }, display: { name: '陈列类', effect: 'dealRate' }, atmosphere: { name: '氛围类', effect: 'offlineRate' }, fengshui: { name: '风水类', effect: 'taxReduction' }, statue: { name: '神像类', effect: 'all' } }; class Decoration { constructor(id, name, tier, type, effects) { this.id = id; this.name = name; this.tier = tier; // startup/growth/develop/prosper/flourish/legacy this.type = type; // signboard/display/atmosphere/fengshui/statue this.effects = effects; // { dealRate: 0.01, offlineRate: 0.01, taxReduction: 0.005 } this.unlocked = false; } } -
任务4.2:实现装饰管理类
装饰解锁和效果计算
class DecorationSystem { constructor() { this.decorations = []; this.playerTitle = '小商贩'; // 玩家当前称号 this.activeDecorations = []; // 当前激活的装饰 } // 称号加成 getTitleBonus() { const titleBonuses = { '小商贩': { dealRate: 0.01, offlineRate: 0, taxReduction: 0 }, '行商': { dealRate: 0.02, offlineRate: 0.01, taxReduction: 0 }, '富商': { dealRate: 0.03, offlineRate: 0.02, taxReduction: 0.01 }, '巨贾': { dealRate: 0.04, offlineRate: 0.03, taxReduction: 0.02 }, '商业大亨': { dealRate: 0.05, offlineRate: 0.04, taxReduction: 0.03 }, '大富翁': { dealRate: 0.06, offlineRate: 0.05, taxReduction: 0.03 } }; return titleBonuses[this.playerTitle] || titleBonuses['小商贩']; } // 装饰加成 getDecorationBonus() { let total = { dealRate: 0, offlineRate: 0, taxReduction: 0 }; this.activeDecorations.forEach(decId => { const dec = this.decorations.find(d => d.id === decId); if (dec) { total.dealRate += dec.effects.dealRate || 0; total.offlineRate += dec.effects.offlineRate || 0; total.taxReduction += dec.effects.taxReduction || 0; } }); return total; } // 获取总加成 getTotalBonus() { const titleBonus = this.getTitleBonus(); const decBonus = this.getDecorationBonus(); return { dealRate: titleBonus.dealRate + decBonus.dealRate, offlineRate: titleBonus.offlineRate + decBonus.offlineRate, taxReduction: titleBonus.taxReduction + decBonus.taxReduction }; } } -
任务4.3:实现称号解锁
消耗凭证解锁称号
class DecorationSystem { // ... 其他方法 // 解锁称号 unlockTitle(title, player) { const requirements = { '小商贩': { startup: 1000 }, '行商': { growth: 1500 }, '富商': { develop: 2000 }, '巨贾': { prosper: 2500 }, '商业大亨': { flourish: 3000 }, '大富翁': { legacy: 5000 } }; const req = requirements[title]; if (!req) return { success: false, message: '称号不存在' }; // 检查凭证是否足够 for (let [tier, amount] of Object.entries(req)) { if ((player.vouchers[tier] || 0) < amount) { return { success: false, message: `${DECORATION_TIERS[tier].name}凭证不足` }; } } // 扣除凭证 for (let [tier, amount] of Object.entries(req)) { player.vouchers[tier] -= amount; } // 更新称号 this.playerTitle = title; // 解锁对应装饰 this.unlockDecorationsByTier(Object.keys(req)[0]); return { success: true, title: title }; } unlockDecorationsByTier(tier) { this.decorations.forEach(dec => { if (dec.tier === tier) { dec.unlocked = true; } }); } }
验证方法
测试称号解锁流程,验证装饰效果正确叠加。
第五阶段:离线收益模块
-
任务5.1:保存离线状态
记录玩家离线时的摊位状态
function saveOfflineState(player, stallManager) { const state = { offlineTime: Date.now(), activeStalls: [] }; // 保存所有活跃摊位 stallManager.stalls.forEach(stall => { if (stall.owner === player.id && stall.status === 'occupied') { state.activeStalls.push({ stallId: stall.id, street: stall.street, index: stall.index, listedItems: stall.listedItems.map(item => ({ itemId: item.itemId, price: item.price, quantity: item.quantity })) }); } }); localStorage.setItem('offlineState', JSON.stringify(state)); return state; } function loadOfflineState() { const saved = localStorage.getItem('offlineState'); return saved ? JSON.parse(saved) : null; } -
任务5.2:计算离线收益
根据离线时长和摊位状态计算收益
function calculateOfflineEarnings(offlineState, decorationSystem) { const offlineMs = Math.min( Date.now() - offlineState.offlineTime, 24 * 60 * 60 * 1000 // 最长24小时 ); const offlineHours = offlineMs / (60 * 60 * 1000); // 离线效率 const efficiencyTiers = [ { maxHours: 4, efficiency: 1.0 }, { maxHours: 8, efficiency: 0.8 }, { maxHours: 12, efficiency: 0.6 }, { maxHours: 24, efficiency: 0.4 } ]; let efficiency = 0.4; for (let tier of efficiencyTiers) { if (offlineHours <= tier.maxHours) { efficiency = tier.efficiency; break; } } // 装饰加成 const bonus = decorationSystem.getTotalBonus(); // 计算每个摊位收益 let totalEarnings = 0; const stallEarnings = []; offlineState.activeStalls.forEach(stall => { let stallEarning = 0; stall.listedItems.forEach(item => { // 预估销售数量 const avgSellTime = 2 * 60 * 60 * 1000; // 平均2小时卖出 const expectedSales = Math.min( item.quantity, Math.floor(offlineMs / avgSellTime) ); // 位置加成 const posBonus = stall.index < 50 ? 1.5 : stall.index < 100 ? 1.25 : 1.0; // 计算收益 const itemEarnings = expectedSales * item.price * 0.95 * posBonus; stallEarning += itemEarnings; }); stallEarning *= efficiency * (1 + bonus.offlineRate); stallEarnings.push({ stallId: stall.stallId, earnings: Math.floor(stallEarning) }); totalEarnings += stallEarning; }); return { totalEarnings: Math.floor(totalEarnings), stallEarnings: stallEarnings, offlineHours: offlineHours, efficiency: efficiency }; } -
任务5.3:显示离线结算界面
玩家上线时展示离线收益
function showOfflineSummary(earnings, player) { if (earnings.offlineHours < 0.1) return; // 少于6分钟不显示 const modal = document.createElement('div'); modal.className = 'offline-summary-modal'; modal.innerHTML = ` `; document.body.appendChild(modal); // 领取收益 window.claimOfflineEarnings = function() { player.money += earnings.totalEarnings; modal.remove(); savePlayerData(player); }; }
验证方法
模拟离线场景,验证收益计算正确,结算界面正常显示。
第六阶段:UI界面模块
-
任务6.1:实现摊位选择界面
可视化展示街道和摊位
// HTML结构 <div class="stall-selector"> <div class="street-tabs"> <button class="street-tab" data-street="east">东街</button> <button class="street-tab" data-street="south">南街</button> <button class="street-tab" data-street="west">西街</button> <button class="street-tab" data-street="north">北街</button> </div> <div class="stall-grid" id="stallGrid"></div> </div> // 渲染摊位网格 function renderStallGrid(street, stallManager) { const grid = document.getElementById('stallGrid'); const stalls = stallManager.stalls.filter(s => s.street === street); grid.innerHTML = stalls.map(stall => ` <div class="stall-cell ${stall.status}" data-id="${stall.id}" onclick="selectStall('${stall.id}')"> <span class="stall-index">${stall.index + 1}</span> ${stall.status === 'occupied' ? '<span class="occupied-mark">占</span>' : ''} </div> `).join(''); } // CSS样式 .stall-grid { display: grid; grid-template-columns: repeat(20, 1fr); gap: 2px; } .stall-cell { aspect-ratio: 1; background: #e8f5e9; border: 1px solid #c8e6c9; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 10px; } .stall-cell.occupied { background: #ffcdd2; border-color: #ef9a9a; cursor: not-allowed; } .stall-cell.selected { border: 2px solid #2196f3; } -
任务6.2:实现商品上架界面
从背包选择商品上架
function showListingModal(player, selectedStall) { const modal = document.createElement('div'); modal.className = 'listing-modal'; modal.innerHTML = ` <div class="modal-content"> <h2>上架商品</h2> <div class="inventory-list"> ${player.inventory.map(item => ` <div class="inventory-item" data-id="${item.id}"> <span class="item-name">${item.name}</span> <span class="item-qty">x${item.quantity}</span> <input type="number" class="list-price" placeholder="售价"> <input type="number" class="list-qty" placeholder="数量" max="${item.quantity}"> </div> `).join('')} </div> <div class="modal-actions"> <button onclick="confirmListing()">确认上架</button> <button onclick="closeModal()">取消</button> </div> </div> `; document.body.appendChild(modal); } -
任务6.3:实现摊位信息面板
显示当前摊位状态和收益
// HTML结构 <div class="stall-info-panel"> <h3>摊位信息</h3> <div class="info-row"> <span>位置:</span> <span id="stallLocation">南街 第15号</span> </div> <div class="info-row"> <span>状态:</span> <span id="stallStatus">营业中</span> </div> <h4>上架商品</h4> <div class="listed-items" id="listedItems"></div> <h4>今日收益</h4> <div class="earnings-summary"> <span>已售:<span id="soldCount">5</span>件</span> <span>收益:<span id="todayEarnings">1500</span>文</span> </div> <div class="panel-actions"> <button onclick="endStall()">收摊</button> </div> </div>
验证方法
测试摊位选择、商品上架、信息面板显示是否正常。
完成清单
完成以下所有任务即表示摆摊系统开发成功:
| 阶段 | 任务数 | 状态 |
|---|---|---|
| 第一阶段:摊位管理模块 | 3 | 待完成 |
| 第二阶段:商品交易模块 | 3 | 待完成 |
| 第三阶段:价格机制模块 | 3 | 待完成 |
| 第四阶段:装饰系统模块 | 3 | 待完成 |
| 第五阶段:离线收益模块 | 3 | 待完成 |
| 第六阶段:UI界面模块 | 3 | 待完成 |
| 总计 | 18 | 0/18 完成 |
开发提示
- 建议按顺序完成各阶段,后续阶段依赖前面的基础
- 每完成一个任务,在代码中标记为已完成
- 摊位自动对齐是核心机制,需要仔细测试
- 价格机制需要与游戏时间系统配合
摆摊系统模拟器
输入参数,模拟计算摆摊的各项费用和收益。