From 69a7aaf5a69bb82a29c4562961aea907a04c862b Mon Sep 17 00:00:00 2001 From: FedoraHat Date: Wed, 23 Jul 2025 09:57:43 +0800 Subject: [PATCH 01/23] feat: add run.sh and run.bat with interactive npm script menu --- run.bat | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ run.sh | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 run.bat create mode 100644 run.sh diff --git a/run.bat b/run.bat new file mode 100644 index 00000000..32ab3163 --- /dev/null +++ b/run.bat @@ -0,0 +1,72 @@ +@echo off +setlocal enabledelayedexpansion + +:menu +cls +echo ================================ +echo JUNON PROJECT RUNNER +echo ================================ +echo. +echo Available Operations: +echo. +echo 1) Server (with nodemon ^& debug) +echo 2) Client +echo 3) Server Only (dev mode) +echo 4) Matchmaker +echo 5) Client Build +echo 6) Database Setup +echo. +echo 0) Exit +echo. +set /p choice="Please select an option (0-6): " + +if "%choice%"=="1" goto server +if "%choice%"=="2" goto client +if "%choice%"=="3" goto serveronly +if "%choice%"=="4" goto matchmaker +if "%choice%"=="5" goto clientbuild +if "%choice%"=="6" goto dbsetup +if "%choice%"=="0" goto exit +echo Invalid option. Please try again. +timeout /t 2 >nul +goto menu + +:server +echo Starting Server with nodemon and debug... +npm run server +goto end + +:client +echo Starting Client... +npm run client +goto end + +:serveronly +echo Starting Server Only (dev mode)... +npm run serveronly +goto end + +:matchmaker +echo Starting Matchmaker... +npm run matchmaker +goto end + +:clientbuild +echo Building Client... +npm run client:build +goto end + +:dbsetup +echo Setting up Database... +npm run db:setup +goto end + +:exit +echo Goodbye! +exit /b 0 + +:end +echo. +echo Operation completed. Press Enter to return to menu... +pause >nul +goto menu \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 00000000..d68ccfb4 --- /dev/null +++ b/run.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +# Colors for better UI +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Function to display the menu +show_menu() { + clear + echo -e "${CYAN}================================${NC}" + echo -e "${CYAN} JUNON PROJECT RUNNER${NC}" + echo -e "${CYAN}================================${NC}" + echo "" + echo -e "${YELLOW}Available Operations:${NC}" + echo "" + echo -e "${GREEN}1)${NC} Server (with nodemon & debug)" + echo -e "${GREEN}2)${NC} Client" + echo -e "${GREEN}3)${NC} Server Only (dev mode)" + echo -e "${GREEN}4)${NC} Matchmaker" + echo -e "${GREEN}5)${NC} Client Build" + echo -e "${GREEN}6)${NC} Database Setup" + echo "" + echo -e "${RED}0)${NC} Exit" + echo "" + echo -e "${BLUE}Please select an option (0-6):${NC} " +} + +# Function to run the selected operation +run_operation() { + case $1 in + 1) + echo -e "${GREEN}Starting Server with nodemon and debug...${NC}" + npm run server + ;; + 2) + echo -e "${GREEN}Starting Client...${NC}" + npm run client + ;; + 3) + echo -e "${GREEN}Starting Server Only (dev mode)...${NC}" + npm run serveronly + ;; + 4) + echo -e "${GREEN}Starting Matchmaker...${NC}" + npm run matchmaker + ;; + 5) + echo -e "${GREEN}Building Client...${NC}" + npm run client:build + ;; + 6) + echo -e "${GREEN}Setting up Database...${NC}" + npm run db:setup + ;; + 0) + echo -e "${YELLOW}Goodbye!${NC}" + exit 0 + ;; + *) + echo -e "${RED}Invalid option. Please try again.${NC}" + sleep 2 + return 1 + ;; + esac +} + +# Main loop +while true; do + show_menu + read -r choice + + if run_operation "$choice"; then + echo "" + echo -e "${PURPLE}Operation completed. Press Enter to return to menu...${NC}" + read -r + fi +done \ No newline at end of file From 87f2bc219952a96e3d61109d4b0a2209e5285741 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Thu, 24 Jul 2025 20:13:30 +0800 Subject: [PATCH 02/23] Added more math functions and $getAngle --- packages/junon-io/client/main.ejs | 35 +++++++++++ .../junon-io/server/entities/event_handler.js | 60 ++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 7dd85d48..b4122f71 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -973,6 +973,36 @@ value1, value2, ... , valueN Returns the smallest value among the given arguments. Ex. $min(1,2,3,4,5) = 1 + + $sin + degrees + Calculates the sine of an angle in degrees. Ex. $sin(30) = 0.5 + + + $cos + degrees + Calculates the cosine of an angle in degrees. Ex. $cos(0) = 1 + + + $tan + degrees + Calculates the tangent of an angle in degrees. Ex. $tan(45) = 1 + + + $asin + value (-1 ~ 1) + Calculates the arc sine in degrees. Ex. $asin(0.5) = 30 + + + $acos + value (-1 ~ 1) + Calculates the arc cosine in degrees + + + $atan + value + Calculates the arc tangent in degrees. + $isLoggedIn $player @@ -1018,6 +1048,11 @@ None Returns the total count of all mobs in the game sector (hostile + neutral). + + $getAngle + $entityId + Gets the current facing angle of an entity in degrees. +
diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index 9a9e84f6..4301b01f 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -226,6 +226,57 @@ class EventHandler { return Math.max(...values.map(v => this._safeNumber(v))) } + sin(degrees) { + const deg = this._safeNumber(degrees) + return this._fixFloat(Math.sin(deg * Math.PI / 180)) + } + + cos(degrees) { + const deg = this._safeNumber(degrees) + return this._fixFloat(Math.cos(deg * Math.PI / 180)) + } + + tan(degrees) { + const deg = this._safeNumber(degrees) + if (Math.abs(deg % 180) === 90) return NaN + return this._fixFloat(Math.tan(deg * Math.PI / 180)) + } + + asin(value) { + const num = this._safeNumber(value) + if (num < -1 || num > 1) return NaN + return this._fixFloat(Math.asin(num) * 180 / Math.PI) + } + + acos(value) { + const num = this._safeNumber(value) + if (num < -1 || num > 1) return NaN + return this._fixFloat(Math.acos(num) * 180 / Math.PI) + } + + atan(value) { + const num = this._safeNumber(value) + return this._fixFloat(Math.atan(num) * 180 / Math.PI) + } + + getAngle(entityId) { + let player = this.getPlayer(entityId) + let angle = 0 + + if (player && typeof player.angle === 'number') { + angle = player.angle + } else { + let entity = this.game.getEntity(entityId) + if (entity && typeof entity.angle === 'number') { + angle = entity.angle + } + } + + //Normalize angle to 0-360 degrees, if this is not added, the angle can be greater than 360, idk why. + angle = ((angle % 360) + 360) % 360 + return angle + } + length(value) { return value.toString().length } @@ -1192,7 +1243,14 @@ class EventHandler { "$getPlatformByCoords": true, "$getStructureByCoords": true, "$hasEffect": true, - "$getTotalMobCount": true + "$getTotalMobCount": true, + "$getAngle": true, + "$sin": true, + "$cos": true, + "$tan": true, + "$asin": true, + "$acos": true, + "$atan": true, } } From c818607dc5c8aaa5e12253a4f1eaf789144a9e44 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 27 Jul 2025 12:23:45 +0800 Subject: [PATCH 03/23] Traditional Chinese translation idk why some of words isn't translated in game --- packages/junon-io/client/main.ejs | 1 + packages/junon-io/client/src/main.js | 3 + .../junon-io/common/translations/zh-Hant.json | 807 ++++++++++++++++++ packages/junon-io/server/server.js | 2 + 4 files changed, 813 insertions(+) create mode 100644 packages/junon-io/common/translations/zh-Hant.json diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index b4122f71..5de7b128 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -120,6 +120,7 @@ +
diff --git a/packages/junon-io/client/src/main.js b/packages/junon-io/client/src/main.js index 2617bba0..f617da29 100644 --- a/packages/junon-io/client/src/main.js +++ b/packages/junon-io/client/src/main.js @@ -21,6 +21,7 @@ const Polyglot = require('node-polyglot') const japaneseTranslationMap = require("../../common/translations/ja") const englishTranslationMap = require("../../common/translations/en") const russianTranslationMap = require("../../common/translations/ru") +const trationalChineseTranslationMap = require("../../common/translations/zh-Hant") const ObjectPool = require("../../common/entities/object_pool") const TeamEntry = require("./components/team_entry") const Lighting = require("./entities/lighting") @@ -141,6 +142,8 @@ class Main { phrases = Object.assign({}, phrases, japaneseTranslationMap) } else if (language === "ru") { phrases = Object.assign({}, phrases, russianTranslationMap) + } else if (language === "zh-Hant") { + phrases = Object.assign({}, phrases, trationalChineseTranslationMap) } } diff --git a/packages/junon-io/common/translations/zh-Hant.json b/packages/junon-io/common/translations/zh-Hant.json new file mode 100644 index 00000000..49c1f735 --- /dev/null +++ b/packages/junon-io/common/translations/zh-Hant.json @@ -0,0 +1,807 @@ +{ + "New Game": "新遊戲", + "Visit Colony": "訪問殖民地", + "Tutorial": "教學模式", + "Starting": "載入中", + "Load Game": "載入遊戲中", + "Login": "登入", + "Logout": "登出", + "Back": "返回", + "Online": "線上", + "Offline": "離線", + "Initializing": "初始化中", + "Last Played At": "最後遊玩時間", + "Public": "公開", + "Private": "私人", + "Start Game": "開始遊戲", + "Welcome": "歡迎", + "Awoken The Spiders You Have": "蜘蛛已被驚醒", + "Unable to Find Spawn Position for Trader. Skipping Event": "無法找到商人生成位置,已跳過該事件", + "Raid Incoming": "襲擊即將來臨", + "Spider Infestation": "蜘蛛入侵中", + "Respawn": "重生", + "Iron Ore": "鐵礦", + "Iron Bar": "鐵錠", + "Copper Ore": "銅礦", + "Copper Bar": "銅錠", + "Circuit Board": "電路板", + "Cloth": "布料", + "Sand": "沙子", + "Glass": "玻璃", + "Wood": "木材", + "Bottle": "瓶子", + "Water Bottle": "水瓶", + "Blood Bottle": "血瓶", + "Wire": "電線", + "Potato": "馬鈴薯", + "Potato Seed": "馬鈴薯種子", + "Plant Fiber": "植物纖維", + "Fiber Seed": "纖維種子", + "Wheat": "小麥", + "Wheat Seed": "小麥種子", + "Gel": "凝膠", + "Alien Meat": "外星生物肉", + "Animal Meat": "動物肉", + "Human Meat": "人肉", + "Gelatin": "明膠", + "Steak": "牛排", + "Hot Dog": "熱狗", + "Vegan Pizza": "素食披薩", + "Slimy Meat Pizza": "黏滑肉披薩", + "Beer": "啤酒", + "Cola": "可樂", + "First Aid Kit": "急救箱", + "Antidote": "解毒劑", + "Stimpack": "興奮劑", + "Mop": "拖把", + "Pistol": "手槍", + "Shotgun": "霰彈槍", + "Bullet Ammo": "子彈", + "Bullet Damage": "子彈傷害", + "Fire Extinguisher": "滅火器", + "Flame Thrower": "火焰噴射器", + "Lighter": "打火機", + "Survival Tool": "生存工具", + "Drill": "鑽頭", + "Katana": "武士刀", + "Space Suit": "太空服", + "Furnace": "熔爐", + "Butcher Table": "屠宰台", + "Stove": "爐灶", + "Brewery": "釀造機", + "Chemistry Station": "化學工作站", + "Mining Drill": "採礦鑽機", + "Ammo Printer": "彈藥製造機", + "Refinery": "精煉機", + "Wall": "牆壁", + "Spike Trap": "尖刺陷阱", + "Manual Airlock": "手動氣閘", + "Airlock": "氣閘門", + "Lamp": "燈", + "Steel Crate": "鋼鐵箱", + "Water Pump": "抽水泵", + "Liquid Pipe": "液體管道", + "Liquid Tank": "液體儲存槽", + "Suit Station": "太空服站", + "Beacon": "信標", + "Mini Turret": "小型砲塔", + "Air Alarm": "空氣警報器", + "Ventilator": "通風機", + "Oxygen Generator": "製氧機", + "Small Oxygen Tank": "小型氧氣罐", + "Gas Pipe": "氣體管道", + "Bed": "床", + "Table": "桌子", + "Trading Table": "貿易桌", + "Chair": "椅子", + "Cage": "籠子", + "Television": "電視", + "Pot": "盆栽", + "Lattice": "格柵地板", + "Steel Floor": "鋼鐵地板", + "Soil": "土壤", + "Wood Floor": "木地板", + "Carpet Floor": "地毯地板", + "Tiled Floor": "磁磚地板", + "Purple Floor": "紫色地板", + "Plated Floor": "鍍層地板", + "Stripe Plated Floor": "條紋鍍層地板", + "Green Floor": "綠色地板", + "Rail Track": "軌道", + "Rail Stop": "電車站", + "Oil Refinery": "石油鑽探精煉機", + "Power Generator": "燃料發電機", + "Fuel Pipe": "燃料管道", + "Fuel Tank": "燃料槽", + "Solar Panel": "太陽能板", + "Remaining": "剩餘", + "Owner": "擁有者", + "Stored": "儲存電量", + "Production": "產電量", + "Power Needed": "所需電力", + "Slime": "史萊姆", + "Slime Corpse": "史萊姆屍體", + "Brood": "寄生蟲", + "Brood Corpse": "寄生蟲屍體", + "Spider": "蜘蛛", + "Spider Corpse": "蜘蛛屍體", + "Monkey": "猴子", + "Monkey Corpse": "猴子屍體", + "Chicken": "雞", + "Chicken Corpse": "雞屍體", + "Clean Bot": "清潔機器人", + "Clean Bot Corpse": "損壞的清潔機器人", + "Trader": "商人", + "Empire": "帝國", + "Guard": "守衛", + "Chemist": "化學家", + "Chemist Corpse": "化學家屍體", + "Messenger": "收稅使者", + "Messenger Corpse": "收稅使者屍體", + "O2": "氧氣濃度", + "Crafting": "製作", + "Ingredients": "材料", + "Suit": "太空服", + "Inventory": "物品欄", + "Map": "地圖", + "Colony": "殖民地資訊", + "Name": "殖民地名稱", + "Location": "位置", + "Members": "成員", + "Days Alive": "存活天數", + "Alliance": "對外關係資訊", + "Status": "狀態", + "Friendly": "友好", + "Neutral": "中立", + "Hostile": "敵對", + "Relationship": "友好度", + "Insufficient Power": "電力不足", + "Generates Iron Ore": "正在生成鐵礦", + "Resume": "繼續遊戲", + "Settings": "設定", + "Exit Game": "結束遊戲", + "Sound": "音效", + "Volume": "音量", + "Controls": "控制", + "Move Up": "向上移動", + "Move Left": "向左移動", + "Move Down": "向下移動", + "Move Right": "向右移動", + "Rotate": "旋轉", + "Craft": "製作", + "Interact": "互動", + "Attack": "攻擊", + "Chat": "聊天", + "Camera Mode": "相機模式", + "Stats View": "通訊狀態檢視", + "Press Any Key": "請按下要設定的按鍵", + "Trade": "交易", + "Sell": "賣出", + "Trader has arrived": "商人已抵達", + "Tax Officer Has Arrived": "收稅使者已抵達", + "Confirm": "確認", + "Needs Fuel": "需要補充燃料", + "Bullet Ammo Required": "需要子彈", + "Destination": "目的地", + "Go": "出發", + "No Passengers on Tram": "電車上沒有任何東西", + "Web": "蜘蛛網", + "Web Trap": "黏網陷阱", + "Sign": "標誌牌", + "Happiness": "幸福度", + "Make Pet": "設為寵物", + "Place ore in left slot to refine gold": "將礦石放入左側槽以精煉黃金", + "You Died": "你已死亡!", + "Blood Pack": "血袋", + "Lead Pipe": "鐵管", + "Cryo Tube": "冷凍艙", + "Oxygen Tank": "氧氣罐", + "Generates Copper Ore": "正在生成銅礦", + "Buy": "購買", + "ColonyName": "殖民地名稱", + "ColonyDay": "經過天數", + "ColonyOnline": "人數", + "PlayersOnline": "在線人數", + "TutorialWelcomeMessage": "這是用來了解如何供電以及為房間補充氧氣的示範基地。按 [C] 鍵打開製作選單,查看各個裝置的功能。", + "Potato.Description": "生存的主食。", + "PotatoSeed.Description": "種在土壤中可長成馬鈴薯。", + "PlantFiber.Description": "製作藥品等材料。", + "FiberSeed.Description": "種在土壤中可長成纖維植物。", + "Wheat.Description": "未加工的小麥,可用於製作麵包等。", + "WheatSeed.Description": "種在土壤中可長成小麥。", + "Gel.Description": "可加工成明膠。", + "AlienMeat.Description": "生的外星生物肉,建議烹煮後再食用。", + "AnimalMeat.Description": "生的動物肉,烹煮後更美味。", + "HumanMeat.Description": "生的人肉。絕對不要吃!?", + "Gelatin.Description": "富含蛋白質,美味。", + "Steak.Description": "烹煮過的肉,別問是什麼肉。", + "HotDog.Description": "烹煮過的動物肉。美式經典食物", + "VeganPizza.Description": "素食者也能享用的美味披薩。", + "SlimyMeatPizza.Description": "由史萊姆和動物肉製成的異國風味披薩。", + "LectersDinner.Description": "烹煮過的人肉。令人印象深刻的味道。", + "Beer.Description": "恢復30點體力。注意別喝過量。", + "Cola.Description": "恢復10點體力。", + "FirstAidKit.Description": "恢復50點生命值。", + "Antidote.Description": "治療中毒效果。", + "BloodPack.Description": "恢復50點生命值。", + "Stimpack.Description": "犧牲25點生命值,換取15秒內攻擊速度加倍。", + "Entity.Health": "耐久度", + "PlantGrowth": "成長度", + "DayCount": "第%{day}天", + "HourCount": "%{hour}時", + "LeadPipe.Description": "基本近戰武器。攻擊力:7", + "Mop.Description": "用於清潔地板的工具。", + "Pistol.Description": "可遠距離攻擊的武器。攻擊力:10", + "Shotgun.Description": "一次發射多發散射彈丸的武器。每顆彈丸造成12點傷害。", + "FireExtinguisher.Description": "可用來滅火。", + "FlameThrower.Description": "可噴射火焰攻擊。", + "Bottle.Description": "可用來盛裝液體。", + "Lighter.Description": "可用來點火。需從燃料槽補充燃料。", + "SurvivalTool.Description": "可採礦或拆除建築物。攻擊力:2", + "Drill.Description": "採礦速度比生存工具更快。攻擊力:5", + "Katana.Description": "可同時攻擊三個敵人。攻擊力:15", + "SpaceSuit.Description": "在太空中必須穿著以防窒息。", + "Furnace.Description": "可將礦石精煉成其他物品。", + "ButcherTable.Description": "可將屍體拆解以獲得肉類和其他資源。", + "Stove.Description": "在此處可進行烹飪。", + "Brewery.Description": "製作飲料解渴。", + "ChemistryStation.Description": "可製作各種化學藥品。", + "MiningDrill.Description": "自動從地下開採礦石。", + "AmmoPrinter.Description": "可製作武器彈藥。", + "Refinery.Description": "將礦石熔煉成黃金(Gold)。", + "Wall.Description": "用於圍出房間。不透氣。", + "SpikeTrap.Description": "對踩到陷阱的敵人造成傷害。", + "ManualAirlock.Description": "手動開關的氣閘。無需電力即可操作。", + "Airlock.Description": "不透氣的門。使用它可以使內部區域充滿氧氣。", + "Lamp.Description": "照亮周圍環境。", + "WaterPump.Description": "抽水源的水。必須設置在水源附近並用管道連接。", + "LiquidPipe.Description": "連接液體相關裝置以輸送液體。", + "LiquidTank.Description": "儲存液體。", + "CryoTube.Description": "復活並治療放入其中的生物。", + "SuitStation.Description": "可存放太空服。", + "Beacon.Description": "將成為訪問你殖民地的玩家生成點。", + "MiniTurret.Description": "防禦用砲塔。需要電力和彈藥。每發子彈攻擊力:12", + "AirAlarm.Description": "當室內沒有氧氣時會亮紅燈警報。", + "Ventilator.Description": "可將氧氣注入密封的房間。必須連接氣體管道。", + "OxygenGenerator.Description": "從水中產生氧氣。", + "SmallOxygenTank.Description": "儲存少量氧氣。必須連接氣體管道。", + "GasPipe.Description": "連接氣體相關裝置以輸送氣體。", + "Bed.Description": "在上面睡覺可恢復體力。。", + "Table.Description": "裝飾用桌子。", + "TradingTable.Description": "設置後可吸引商人前來。", + "Chair.Description": "裝飾用椅子。", + "Cage.Description": "用於收容馴服的動物或外星生物。", + "Television.Description": "裝飾用電視。", + "Pot.Description": "可放置花草的裝飾用花盆。", + "Lattice.Description": "用於在外部放置裝置的輕量地板。只能放置在小行星區域外(如太空上)", + "SteelFloor.Description": "可在上面行走。也用於在水面、石油上或太空中放置裝置。", + "Soil.Description": "用於種植農作物。", + "WoodFloor.Description": "木質地板。", + "CarpetFloor.Description": "鋪有紅地毯的地板。", + "TiledFloor.Description": "磁磚地板。", + "PurpleFloor.Description": "紫色地板。", + "PlatedFloor.Description": "鍍層地板。", + "StripePlatedFloor.Description": "條紋鍍層地板。", + "GreenFloor.Description": "綠色地板。", + "RailTrack.Description": "電車行駛的軌道。", + "RailStop.Description": "電車出發和停靠的車站。必須設置在軌道末端。", + "Wire.Description": "在裝置間供電。", + "OilRefinery.Description": "從地下開採石油並精煉成化學燃料。", + "PowerGenerator.Description": "使用化學燃料發電。", + "FuelPipe.Description": "連接裝置以輸送燃料。", + "FuelTank.Description": "儲存燃料。", + "AllianceName": "勢力名稱", + "taxes_not_paid.Description": "未繳稅", + "cannibalism.Description": "食用了帝國士兵的肉", + "extreme_cannibalism.Description": "吞噬了大量帝國士兵的肉", + "RailStationName": "電車站名稱", + "WebTrap.Description": "使敵人被困住5秒無法動彈的陷阱。", + "Sign.Description": "可在上面書寫想讓其他玩家看到的文字。", + "RifleAmmo.Description": "突擊步槍用彈藥。", + "Grenade.Description": "爆炸並對敵人造成傷害的投擲武器。", + "PoisonGrenade.Description": "在一定區域產生毒氣的投擲武器。", + "Web.Description": "黏性物質,可用來製作物品。", + "Gold.Description": "用於買賣物品或接受服務的貨幣。", + "to save your progress": "即可儲存進度", + "SurvivalWelcomeMessage": "建立殖民地,建造你的太空要塞。敵人會在夜晚出現。", + "IronBar.Description": "鐵錠。", + "CopperBar.Description": "銅錠。", + "CircuitBoard.Description": "電路板。", + "Cloth.Description": "用於製作家具和裝備的材料。", + "Sand.Description": "製作特定物品的材料。", + "Glass.Description": "玻璃。", + "Wood.Description": "製作家具等物品的材料。", + "Assault Rifle": "突擊步槍", + "Bio Raptor": "生化迅猛龍", + "Bio Raptor Corpse": "生化迅猛龍屍體", + "Car":"汽車", + "Car Corpse": "汽車殘骸", + "SteelCrate.Description": "可儲存物品。", + "SolarPanel.Description": "利用太陽能發電。只能放置在格柵(Lattice)上", + "Messenger.DemandPayment": "以我們神聖帝國的名義,命令你繳納稅金作為忠誠的證明!", + "Messenger.RightChoice": "明智的選擇!", + "Messenger.Blasphemy": "褻瀆我們神聖帝國的叛徒!", + "Messenger.Choices.ProceedPay": "支付 %{gold} Gold。", + "Messenger.Choices.RefusePay": "拒絕支付。", + "Shotgun Shell": "霰彈槍彈藥", + "Rifle Ammo": "步槍彈藥", + "Livestock": "牲畜", + "WaterBottle.Description": "可將水裝入製氧機,或用於澆灌植物。", + "BloodBottle.Description": "內含血液。", + "BulletAmmo.Description": "手槍和砲塔使用的彈藥。", + "OutdatedGameClient": "遊戲用戶端已過期。請重新整理頁面。", + "NetworkOutdatedGameClient": "遊戲用戶端已過期。請重新整理頁面。", + "Escape Pod": "逃生艙", + "Core": "核心", + "Core.Description": "電源。可提供500W電力。", + "Connecting": "連接中", + "DisconnectedReconnecting": "連線已中斷。將在 %{seconds} 秒後重新連線。", + "Taming": "馴服度", + "Following": "跟隨中", + "Take Along": "使其跟隨", + "Release": "釋放", + "Message": "訊息", + "Done": "完成", + "Edit": "編輯", + "Cancel": "取消", + "Cook": "烹飪", + "Brew": "釀造", + "Place Ammunition": "請裝填彈藥", + "MatchmakerUnavailable": "無法與伺服器同步。請重新整理頁面後重試。", + "Relationship.EmpireFriendly": "帝國目前對你友好。", + "Relationship.EmpireNeutral": "帝國目前對你中立。", + "Relationship.EmpireHostile": "帝國目前對你敵對。", + "Potato Plant": "馬鈴薯植株", + "Fiber Plant": "纖維植物", + "Wheat Plant": "小麥植株", + "Lecters Dinner": "萊克特風晚餐", + "Game Settings": "遊戲設定", + "Zoom In": "放大", + "Zoom Out": "縮小", + "ShotgunShell.Description": "霰彈槍彈藥。", + "Make Livestock": "設為牲畜", + "Events.TaxCollection": "收稅使者來訪", + "Events.RaidWarning": "襲擊倒數", + "Events.Raid": "襲擊", + "Events.RaidEnd": "襲擊結束", + "Events.TraderArrived": "商人已抵達。", + "Events.SlaveTraderArrived": "奴隸商人已抵達。", + "Events.SpiderInfestation": "蜘蛛入侵中", + "Events.MeteorShower": "流星雨", + "Shotgun Shells Required": "需要霰彈槍彈藥。", + "Rifle Ammo Required": "需要步槍彈藥。", + "Corpse Required": "需要屍體。", + "Background Volume": "背景音樂音量", + "Effects Volume": "音效音量", + "No Saves Found": "找不到儲存資料", + "Privacy": "隱私設定", + "Credits": "製作名單", + "Inventory Full": "物品欄已滿。", + "Mining not allowed when owner is offline": "殖民地擁有者離線時不允許採礦。", + "You are not allowed to use flamethrower": "你沒有使用火焰噴射器的權限。", + "You dont have building permissions": "你沒有建築權限。", + "That doesnt belong to you": "此物品不屬於你。", + "You dont have storage viewing permissions": "你沒有查看儲存內容物的權限。", + "Invalid Item": "無法在此處使用此物品。", + "You just ate": "你剛剛才吃過東西。", + "Not enough Gold": "黃金(Gold)不足。", + "Not enough inventory to store gold": "沒有足夠的物品欄空間存放Gold。", + "Not enough items available": "物品不足。", + "Bot Limit Reached": "機器人數量已達上限。", + "Pet Limit Reached": "寵物數量已達上限。", + "Insufficient Blood": "血液不足。", + "Taming Limit Reached": "馴服數量已達上限。", + "The Spiders have been left in peace": "蜘蛛已恢復休眠狀態。", + "Rail Network is currently in use": "此車站目前正在使用中,無法拆除。", + "Player.Kicked": "你已被踢出。", + "ButcherTable.HarvestFailure": "未能成功獲取肉類或其他物品。", + "AssaultRifle.Description": "能快速連續發射子彈的突擊步槍。", + "Entity.Mob.Health": "生命值", + "OxygenTank.Description": "儲存氧氣。必須連接氣體管道。", + "Grenade": "手榴彈", + "AssignedRole": "你已被賦予 %{roleName} 職位", + "GuestRole": "你已被賦予訪客職位", + "MemberRole": "你已被賦予成員職位", + "Visibility": "可見性", + "You are running out of oxygen": "你的氧氣即將耗盡。", + "You are tired and restless": "你感到疲倦不安。", + "You are feeling hungry": "你感到飢餓。", + "IronOre.Description": "鐵礦。", + "CopperOre.Description": "銅礦。", + "VisitOtherColony": "訪問其他殖民地", + "Visit": "訪問殖民地", + "RestartIn": "%{number} 秒後重新開始", + "Guard Corpse": "守衛屍體", + "MemberList": "成員列表", + "Days": "天", + "Poison Grenade": "毒氣彈", + "Yes": "是", + "No": "否", + "Access Denied": "無權進入。", + "Sealed Door": "密封門", + "SealedDoor.Description": "限制特定人物以外進入的門。", + "Unreachable": "無法觸及。", + "Permissions": "權限設定", + "Storage Access": "儲存空間存取權", + "Asteroid Mining": "小行星採礦", + "DemoteMembersPermission": "當殖民地擁有者離開遊戲時,是否將成員降級為訪客", + "Everyone": "所有人", + "ColonyLeaveConfirmMessage": "由於你尚未登入,你的殖民地將被刪除。確定要繼續嗎?", + "Permission": "權限", + "AllowMembers": "成員", + "AllowEveryone": "所有人", + "Send Money": "發送金錢", + "To": "發送給", + "Send": "發送", + "Top": "熱門", + "MineColony": "我的殖民地", + "AnonymousGameWarning": "[警告] 此殖民地將不會儲存。請登入以儲存進度。", + "ColonyFull": "此殖民地人數已達上限。", + "Save and Quit": "儲存並離開", + "Info": "資訊", + "Player.Banned": "你已被禁止進入殖民地。", + "No results": "無結果", + "Items": "物品", + "Manage": "管理", + "VendingStorage": "陳列", + "Purchase": "購買", + "VendingEmpty": "此自動販賣機目前是空的", + "Balance": "餘額", + "Deposit": "存款", + "Withdraw": "提款", + "Large Table": "大桌子", + "LargeTable.Description": "大型裝飾用桌子。", + "Food Vending Machine": "食物自動販賣機", + "Drinks Vending Machine": "飲料自動販賣機", + "Combat Armor": "戰鬥護甲", + "CombatArmor.Description": "使近戰更容易。減少4點傷害。", + "BoughtTooMuch": "你今天已經買了很多了。請明天再試。", + "Yellow Floor": "黃色地板", + "Bronze Floor": "青銅地板", + "Blue Floor": "藍色地板", + "Gray Floor": "灰色地板", + "Grass Floor": "草地地板", + "Pink Floor": "粉紅色地板", + "YellowFloor.Description": "黃色地板。", + "BronzeFloor.Description": "青銅地板。", + "BlueFloor.Description": "藍色地板。", + "GrayFloor.Description": "灰色地板。", + "GrassFloor.Description": "鋪有草皮的地板。", + "PinkFloor.Description": "粉紅色地板。", + "FoodVendingMachine.Description": "可陳列食物進行販售。", + "DrinksVendingMachine.Description": "可陳列飲料進行販售。", + "Atm.Description": "可從帳戶提領或存入Gold。", + "AdminRole": "你已被賦予管理員權限。", + "Coffee Seed": "咖啡種子", + "Sunflower Seed": "向日葵種子", + "Poppy Seed": "罌粟種子", + "Blue Seed": "藍蓮花種子", + "Coffee": "咖啡", + "Coffee.Description": "恢復50點體力。", + "Coffee Bean": "咖啡豆", + "CoffeeBean.Description": "沖泡咖啡的材料。", + "Coffee Plant": "咖啡樹", + "CoffeePlant.Description": "種在土壤中可長成咖啡樹。", + "Sunflower": "向日葵", + "Sunflower.Description": "可裝飾在盆栽裡的花。", + "Poppy": "罌粟花", + "Poppy.Description": "可裝飾在盆栽裡的花。", + "Blue Lotus": "藍蓮花", + "BlueLotus.Description": "可裝飾在盆栽裡的花。", + "SunflowerSeed.Description": "種在土壤中可長成向日葵。", + "BlueSeed.Description": "種在土壤中可長成藍蓮花。", + "PoppySeed.Description": "種在土壤中可長成罌粟花。", + "Flower Required": "需要花朵。", + "Ivory Wall": "象牙白牆壁", + "IvoryWall.Description": "白色牆壁。", + "Green Wall": "綠色牆壁", + "GreenWall.Description": "綠色牆壁。", + "Blue Wall": "藍色牆壁", + "BlueWall.Description": "藍色牆壁。", + "Favorite": "最愛", + "Favorites": "我的最愛", + "New": "新", + "You dont have permission to sell": "你沒有販售物品的權限。", + "Poppy Farm": "罌粟花田", + "BlueLotus Farm": "藍蓮花田", + "Sunflower Farm": "向日葵田", + "PlantFiber Farm": "纖維植物農田", + "CoffeeBean Farm": "咖啡豆農田", + "Wheat Farm": "小麥農田", + "Potato Farm": "馬鈴薯農田", + "Choose Crop to Grow": "選擇要種植的作物", + "ChooseCrop": "[E] 選擇作物", + "Slave Trader": "奴隸商人", + "Trade Slaves": "奴隸貿易", + "Nuu Slave": "努星人奴隸", + "Pixi Slave": "皮克西星人奴隸", + "Garam Slave": "加拉姆星人奴隸", + "Slavers Table": "奴隸貿易桌", + "SlaversTable.Description": "設置後可吸引奴隸商人前來。", + "Farm Controller": "農場控制器", + "FarmController.Description": "放置在土壤附近。可指示奴隸種植何種作物。", + "State": "狀態", + "Tasks": "任務", + "Plant Seed": "播種", + "Water Crop": "澆水", + "Harvest Crop": "收成", + "Butcher Corpse": "屠宰", + "Edit Sign": "編輯標誌牌", + "Access Storage": "存取儲存空間", + "Access Mining Drill": "存取採礦鑽機", + "Refill Turret": "補充砲塔彈藥", + "FarmControllerTip": "奴隸開始播種前,附近需要有種子箱。", + "Play": "遊玩", + "Walkthrough.movement": "按 wasd 鍵移動。", + "Walkthrough.mining": "前往Room2,點擊小行星採集 10鐵礦石。", + "Walkthrough.lead_pipe": "按 C 鍵開啟製作選單,製作 鐵管。", + "Walkthrough.release_guard": "前往Room3,從 冷凍艙 釋放 守衛。將游標指向冷凍艙按 E 鍵釋放。注意守衛會攻擊你。", + "Walkthrough.kill_guard": "擊倒守衛。", + "Walkthrough.grab_potatoes": "前往Room4,從鐵箱取出 馬鈴薯。右鍵點擊開啟儲存箱", + "Walkthrough.eat_potatoes": "吃 馬鈴薯 充飢。", + "Walkthrough.player_permissions": "點擊Dummy Player,右上角會顯示權限選單。點擊顯示 [Guest] 的地方 來更改權限。", + "Walkthrough.dismantle": "裝備 生存工具,將游標指向鐵箱按住點擊,拆除 鐵箱。", + "Walkthrough.tame_slime": "餵食馬鈴薯 馴服 一隻史萊姆。裝備馬鈴薯(拿在手上)並接近史萊姆即可餵食。", + "Walkthrough.kill_slime": "擊倒另一隻史萊姆。", + "Walkthrough.butcher_slime": "將游標指向史萊姆屍體按 E 鍵可拾取。帶著史萊姆屍體前往 屠宰台 進行分解。", + "Walkthrough.cook_slime": "在 爐灶凝膠 烹飪成 明膠。", + "Walkthrough.sleep": "前往下一個房間,在 睡覺 恢復體力。", + "Walkthrough.zoom": "按 -= 鍵放大縮小", + "Walkthrough.suit_station": "從太空服站拿取 太空服。", + "Walkthrough.collect_water": "前往下一個房間。目前製氧機沒有水。使用水瓶可以為製氧機加水。從逃生艙取出 瓶子,從液體儲存槽取水。", + "Walkthrough.fill_oxygen_generator": "為 製氧機 加滿水。加滿水後房間應會充滿氧氣。", + "Walkthrough.refill_space_suit": "將游標指向製氧機按 E 鍵,也可為太空服補充氧氣。", + "Walkthrough.craft_iron_bar": "你也可以自動化氧氣供應過程。只需用液體管道連接液體儲存槽和製氧機。用 熔爐 製作一些 鐵錠。所需的鐵礦石在逃生艙內。", + "Walkthrough.craft_liquid_pipe": "按 C 鍵開啟製作選單,製作 液體管道。也可使用下方的搜尋欄搜尋「液體管道」。", + "Walkthrough.connect_liquid_pipe": "在製氧機和液體儲存槽之間放置 液體管道 進行連接。", + "Walkthrough.rail_tram": "恭喜!房間已充滿氧氣。離開房間,從 車站 搭乘 礦車 前往農場車站。", + "Walkthrough.plant_seed": "從逃生艙取出 小麥種子 種在土壤中。", + "Walkthrough.water_crop": "用瓶子從湖中取水,為種下的種子澆水。澆水可加速作物生長。", + "Walkthrough.farm_controller": "你可以透過指示奴隸來自動化農業。只需在土壤附近放置 農場控制器。從逃生艙取出農場控制器並放置。", + "Walkthrough.select_wheat": "將游標指向農場控制器開啟選單,選擇 小麥 作為要種植的作物。", + "Walkthrough.end": "只要奴隸能從附近的箱子取得種子,他們就會持續種植小麥。如果他們在附近箱子找到瓶子,也會為尚未澆水的作物澆水。", + "AskTutorialMessage": "你想先進行教學嗎?如果你是新手,強烈建議先透過教學了解遊戲基礎。", + "TutorialCreateNewColonyMessage": "恭喜完成教學!現在要建立你自己的太空基地嗎?", + "Unset": "未設定", + "Not enough Ingredients": "材料不足。", + "Slave": "奴隸", + "Dummy Player": "假人玩家", + "Partners": "合作夥伴", + "Permission Denied": "你沒有權限。", + "Local": "本地", + "Global": "全域", + "Guests are not allowed to do that": "訪客不被允許執行此操作。", + "Admin": "管理員", + "Account": "帳戶", + "Transfer": "轉帳", + "Amount": "金額", + "StoveSlaveNotice": "奴隸只會烹飪此種食物", + "Bread": "麵包", + "Bread.Description": "早晨的能量來源。", + "Targets": "目標設定", + "MobTarget": "敵對NPC", + "PlayerTarget": "敵對玩家", + "You dont have permission to mine asteroids": "你沒有採集小行星的權限", + "Team": "隊伍", + "Teams": "隊伍", + "Player": "玩家", + "requests to join your team": "請求加入你的隊伍", + "Upgrade": "升級", + "Request Sent": "請求已發送", + "Join Team": "加入隊伍", + "Accept": "接受", + "Reject": "拒絕", + "Join": "加入", + "Leave": "離開隊伍", + "Restart Character": "重置角色重新開始(*進度將遺失)", + "You just did that recently": "你最近才執行過此操作。", + "Preview": "預覽", + "FlamethrowerTurret": "火焰噴射砲塔", + "Delete Role": "刪除職位", + "New Role": "新增職位", + "RoleName": "職位名稱", + "Build": "建造", + "Mine Asteroids": "採集小行星", + "Plant Seeds": "播種", + "Harvest Crops": "收成農作物", + "Sell To Trader": "出售給商人", + "PeacefulModeDescription": "無需繳稅,也不會被襲擊。適合建造美麗基地或進行角色扮演。", + "SurvivalModeDescription": "不繳稅將導致帝國軍隊襲擊。適合喜歡適度風險和刺激的玩家。", + "HardcoreModeDescription": "帝國軍隊每天都會襲擊。每次襲擊敵人會變得更強。適合挑戰自己生存極限的玩家。", + "GameModeWarning": "注意:遊戲中途無法變更遊戲模式。", + "Choose a Game Mode": "選擇遊戲模式", + "Peaceful": "和平", + "Survival": "生存", + "Hardcore": "硬核", + "AcceptDecide": "選擇並應用模式", + "You must login to use this": "你必須登入才能使用此功能。", + "Fence": "圍欄", + "Fence.Description": "限制寵物、機器人和奴隸移動的門,但玩家可自由通過。", + "Mode": "難度", + "Deconstruct": "拆除", + "Collect Miner": "收集鑽機礦石", + "Deep Drill": "深層鑽機", + "DeepDrill.Description": "從油田地下開採珍貴資源。需要燃料和電力。", + "Flamethrower Turret": "火焰噴射砲塔", + "FlamethrowerTurret.Description": "向目標噴射火焰。啟動需要燃料和電力。", + "Missile Turret": "飛彈砲塔", + "MissileTurret.Description": "發射追蹤飛彈攻擊鎖定的目標。", + "Sulfur Ore": "硫磺礦石", + "SulfurOre.Description": "硫磺礦石", + "Explosives": "炸藥", + "Explosives.Description": "用於製作手榴彈和飛彈。", + "Missile": "飛彈", + "Ammunition for missile turret": "飛彈砲塔彈藥。", + "open": "開啟", + "close": "關閉", + "use": "使用", + "Generates Sulfur Ore": "正在生成硫磺礦石", + "MissileTurret.PlaceAmmunition": "請裝填飛彈。", + "Blue Energy Sword": "藍色能量劍", + "Red Energy Sword": "紅色能量劍", + "Green Energy Sword": "綠色能量劍", + "BlueEnergySword.Description": "由隕石礦鍛造而成,能產生特殊力量的劍。造成25點傷害。", + "RedEnergySword.Description": "由隕石礦鍛造而成,能產生特殊力量的劍。造成25點傷害。", + "GreenEnergySword.Description": "由隕石礦鍛造而成,能產生特殊力量的劍。造成25點傷害。", + "Vodka": "伏特加", + "Vodka.Description": "恢復40點體力。有了它,俄羅斯就是無敵的!", + "Meteorite": "隕石礦", + "Meteor Shower": "流星雨來襲", + "Miasma": "瘴氣", + "Forge": "鍛造爐", + "Forge.Description": "可製作武器和太空服。", + "Disinfectant": "消毒劑", + "Disinfectant.Description": "噴灑可清除瘴氣。", + "Wrench": "扳手", + "Wrench.Description": "可用於修理建築物。", + "Floor": "地板", + "Floor.Description": "可自訂的地板。雙擊可更改顏色和紋理。", + "Sky": "天空", + "Sky.Description": "天空", + "Water": "水", + "Water.Description": "水", + "Oil": "石油", + "Oil.Description": "石油", + "Rock": "地面", + "Rock.Description": "地面", + "Asteroid": "小行星", + "Asteroid.Description": "小行星", + "Copper Asteroid": "銅礦", + "CopperAsteroid.Description": "銅礦", + "Meteorite Asteroid": "隕石礦", + "MeteoriteAsteroid.Description": "隕石礦", + "Report Player": "舉報玩家", + "ReportPlayer.Description": "如果你認為有玩家違反了 %{rules} 中列出的規則,請在此舉報。如有可能,請附上螢幕截圖。無故舉報屬違規行為,可能導致你被禁止。", + "Offending user": "違規用戶名稱", + "Description": "描述", + "Attach screenshot": "附上螢幕截圖", + "Assassins Knife": "刺客匕首", + "AssassinsKnife.Description": "偽裝成通訊器的暗殺匕首。", + "Radio": "通訊器", + "Radio.Description": "通訊器。發現屍體時請用它回報。", + "Bar": "酒吧", + "Engine Room": "引擎室", + "Chemistry Lab": "化學實驗室", + "ExitCamera": "返回", + "Camera": "攝影機", + "Invite": "邀請", + "NeededToStart": "需要 %{count} 名玩家才能開始遊戲。", + "armory": "軍械庫", + "incinerator": "焚化爐", + "Medbay": "醫療室", + "medbay": "醫療室", + "security": "保全室", + "oxygen_supply": "氧氣供應室", + "prison": "監獄", + "corpses": "停屍間", + "chemistry": "化學實驗室", + "meeting": "會議室", + "biology": "生物實驗室", + "engine": "引擎室", + "bar": "酒吧", + "kitchen": "廚房", + "garden": "農業區", + "No player targets available": "附近沒有可攻擊的目標玩家。", + "Voted": "已投票", + "Vote": "投票", + "votes": "票", + "Skip Vote": "跳過投票", + "Vote the Imposter": "投票給你認為是冒名頂替者的人", + "Play Again": "再玩一次", + "Spectate": "觀戰", + "Total Tasks Completed": "任務完成率", + "Host Private Game": "開設私人遊戲", + "FindTheImposter.Title": "尋找冒名頂替者", + "FindTheImposter.Description": "完成所有任務或投票淘汰冒名頂替者即可獲勝。殺死所有船員則冒名頂替者獲勝。", + "CanUseAgain": "%{cooldown} 秒後可再次使用", + "No corpses nearby": "附近沒有屍體。", + "Meeting Room": "會議室", + "Spawn Area": "重生區", + "Oxygen Supply Room": "氧氣供應室", + "Power Switch": "電源開關", + "PowerSwitch.Description": "可控制多個電源。可開啟或關閉。", + "Terminal": "終端機", + "Terminal.Description": "裝飾用電腦,可使用。", + "Underground Vent": "通風管道", + "UndergroundVent.Description": "通風管道。可瞬間移動到另一個管道口。", + "Lava": "熔岩", + "Security Camera": "監視攝影機", + "SecurityCamera.Description": "可監視特定區域。", + "Security Monitor": "監視螢幕", + "SecurityMonitor.Description": "可查看監視攝影機的畫面。", + "Wall Lamp": "壁燈", + "WallLamp.Description": "照亮周圍環境。", + "Custom": "自訂顏色", + "Intensity": "亮度", + "Wood Table": "木桌", + "WoodTable.Description": "木製桌子。", + "Wood Chair": "木椅", + "WoodChair.Description": "木製椅子。", + "Domination.Title": "領土爭奪", + "Domination.Description": "與其他玩家組隊,摧毀敵方基地。", + "Command Blocks": "指令方塊", + "Triggers": "觸發器", + "Logs": "日誌", + "Functions": "函數", + "Function": "函數", + "Enable Triggers": "啟用觸發器", + "New Trigger": "新增觸發器", + "Create": "建立", + "Usage": "用法", + "Function.Usage1": "函數呼叫用法如下:", + "Function.Usage2": "基本上,它會根據輸入值(例如玩家名稱)輸出一個值(例如該玩家的Gold數量)。函數可用於指令或ifelse條件。", + "Arguments": "參數", + "Returns": "回傳值", + "Tesla Coil": "特斯拉線圈", + "TeslaCoil.Description": "向最多4個目標發射閃電。啟動需要電力。", + "Bomber Turret": "轟炸砲塔", + "BomberTurret.Description": "向目標發射炸彈。啟動需要電力和炸彈。", + "Scar17.Description": "發射高速子彈的點射武器。", + "Plasma Gun": "電漿槍", + "PlasmaGun.Description": "發射電漿彈的武器。", + "Plasma Cell": "電漿彈", + "PlasmaCell.Description": "電漿槍彈藥。", + "Rocket Launcher": "火箭發射器", + "RocketLauncher.Description": "向目標發射飛彈的武器。", + "Squid Staff": "魔法杖", + "SquidStaff.Discription": "一次發射5發能量彈的武器。", + "Tree": "樹木", + "Tree.Description": "裝飾用樹木。", + "Christmas Tree": "聖誕樹", + "ChristmasTree.Description": "聖誕樹。", + "Red Present": "紅色禮物", + "RedPresent.Description": "紅色禮物盒。", + "Blue Present": "藍色禮物", + "BluePresent.Description": "藍色禮物盒。", + "Green Present": "綠色禮物", + "GreenPresent.Description": "綠色禮物盒。", + "Pocket Trader": "隨身貿易商", + "PocketTrader.Description": "使用後可在貿易商不在附近時進行交易。", + "Bowl": "碗", + "Bowl.Description": "可用來製作湯品。", + "Omelette": "歐姆蛋", + "Fries": "薯條", + "Potato Soup": "馬鈴薯湯", + "Slime Broth": "史萊姆湯", + "Miso Soup": "味噌湯", + "Energy Drink": "能量飲料", + "EnergyDrink.Description": "飲用後暫時提高移動速度。", + "Alien Juice": "外星果汁", + "AlienJuice.Description": "飲用後暫時隱形。", + "Timer Bomb": "定時炸彈", + "TimerBomb.Description": "設置後將在10秒後爆炸。", + "Hazmat Suit": "防護衣", + "HazmatSuit.Description": "保護你免受瘴氣和毒素傷害", + "Prisoner Suit": "囚犯服", + "PrisonerSuit.Description": "囚犯服裝。", + "Police Suit": "警察制服", + "PoliceSuit.Description": "警察制服。", + "Lab Coat": "實驗袍", + "LabCoat.Description": "研究人員服裝。", + "Cultist Suit": "邪教徒服裝", + "CultistSuit.Description": "邪教領袖服裝。", + "Santa Hat": "聖誕帽", + "Name Tag": "命名牌", + "Missile.Description": "飛彈砲塔和火箭發射器使用的彈藥。", + "Events.Alarm": "修理燈泡" +} \ No newline at end of file diff --git a/packages/junon-io/server/server.js b/packages/junon-io/server/server.js index a2b15870..0cb72ecb 100644 --- a/packages/junon-io/server/server.js +++ b/packages/junon-io/server/server.js @@ -28,6 +28,7 @@ const Polyglot = require('node-polyglot') const englishTranslationMap = require('../common/translations/en') const japaneseTranslationMap = require('../common/translations/ja') const russianTranslationMap = require('../common/translations/ru') +const trationalChineseTranslationMap = require('../common/translations/zh-Hant') const SectorModel = require("junon-common/db/sector") const ObjectPool = require("../common/entities/object_pool") const TileHit = require("./entities/tile_hit") @@ -44,6 +45,7 @@ global.i18n = { this.instances["en"] = new Polyglot({ phrases: englishTranslationMap, allowMissing: true }) this.instances["ja"] = new Polyglot({ phrases: japaneseTranslationMap, allowMissing: true }) this.instances["ru"] = new Polyglot({ phrases: russianTranslationMap, allowMissing: true }) + this.instances["zh-Hant"] = new Polyglot({ phrases: trationalChineseTranslationMap, allowMissing: true }) }, hasLanguage(language) { return this.instances[language] From 997b97e893cd18cd2d15ccf6b6888b4e275506f1 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 27 Jul 2025 18:44:00 +0800 Subject: [PATCH 04/23] Added "raid" flag in /spawnmob --- .../junon-io/server/commands/spawn_mob.js | 34 +++++++++++++++++++ packages/junon-io/server/entities/raid.js | 4 +++ 2 files changed, 38 insertions(+) diff --git a/packages/junon-io/server/commands/spawn_mob.js b/packages/junon-io/server/commands/spawn_mob.js index 67572684..56b00ffe 100644 --- a/packages/junon-io/server/commands/spawn_mob.js +++ b/packages/junon-io/server/commands/spawn_mob.js @@ -129,7 +129,41 @@ class SpawnMob extends BaseCommand { } } + if (keyValueMap["raid"] === "true") { + if (!keyValueMap["attackables"]) { + data.attackables = ["players", "mobs", "buildings"] + } + if (!keyValueMap["status"]) { + let status = Protocol.definition().MobStatus.Hostile + if (typeof status !== 'undefined' && status !== null) { + data.status = status + } + } + } + + if (!keyValueMap["attackables"]) { + data.attackables = ["players", "mobs", "buildings"] + } + const mobs = this.sector.spawnMob(data) + + if (keyValueMap["raid"] === "true") { + const Raid = require("../entities/raid") + const fakeEventManager = this.game.eventManager + const raidData = { + sector: this.sector, + team: caller.getTeam ? caller.getTeam() : undefined, + permanent: true + } + const raid = new Raid(fakeEventManager, raidData) + raid.prepare() + if (!fakeEventManager.raids) fakeEventManager.raids = [] + fakeEventManager.raids.push(raid) + mobs.forEach((mob) => { + mob.setRaid(raid) + }) + } + mobs.forEach((mob) => { mob.onCommandSpawned(caller) diff --git a/packages/junon-io/server/entities/raid.js b/packages/junon-io/server/entities/raid.js index fa00511b..31b30aec 100644 --- a/packages/junon-io/server/entities/raid.js +++ b/packages/junon-io/server/entities/raid.js @@ -71,6 +71,7 @@ class Raid { } isRaidTooLong() { + if (this.data && this.data.permanent) return false let twelveHours = Constants.physicsTimeStep * Constants.secondsPerHour * 12 return (this.game.timestamp - this.occurTimestamp) > twelveHours } @@ -503,6 +504,9 @@ class Raid { } endRaid() { + // mobs with "raid:true" flag in /spawnmob will never end (it has bug in sandbox mode after you spawn mobs with raid:true flag, raid event mob will never despawn) + if (this.data && this.data.permanent) return + if (this.isRaidEnded) { if (this.isRaidTooLong()) { // if i still have mobs alive, remove them immediately From b2034b2eeed783b571d7d7f01718e48ec1185e53 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Tue, 29 Jul 2025 10:32:12 +0800 Subject: [PATCH 05/23] removed undone --- run.bat | 72 -------------------------------------------------- run.sh | 82 --------------------------------------------------------- 2 files changed, 154 deletions(-) delete mode 100644 run.bat delete mode 100644 run.sh diff --git a/run.bat b/run.bat deleted file mode 100644 index 32ab3163..00000000 --- a/run.bat +++ /dev/null @@ -1,72 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -:menu -cls -echo ================================ -echo JUNON PROJECT RUNNER -echo ================================ -echo. -echo Available Operations: -echo. -echo 1) Server (with nodemon ^& debug) -echo 2) Client -echo 3) Server Only (dev mode) -echo 4) Matchmaker -echo 5) Client Build -echo 6) Database Setup -echo. -echo 0) Exit -echo. -set /p choice="Please select an option (0-6): " - -if "%choice%"=="1" goto server -if "%choice%"=="2" goto client -if "%choice%"=="3" goto serveronly -if "%choice%"=="4" goto matchmaker -if "%choice%"=="5" goto clientbuild -if "%choice%"=="6" goto dbsetup -if "%choice%"=="0" goto exit -echo Invalid option. Please try again. -timeout /t 2 >nul -goto menu - -:server -echo Starting Server with nodemon and debug... -npm run server -goto end - -:client -echo Starting Client... -npm run client -goto end - -:serveronly -echo Starting Server Only (dev mode)... -npm run serveronly -goto end - -:matchmaker -echo Starting Matchmaker... -npm run matchmaker -goto end - -:clientbuild -echo Building Client... -npm run client:build -goto end - -:dbsetup -echo Setting up Database... -npm run db:setup -goto end - -:exit -echo Goodbye! -exit /b 0 - -:end -echo. -echo Operation completed. Press Enter to return to menu... -pause >nul -goto menu \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100644 index d68ccfb4..00000000 --- a/run.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -# Colors for better UI -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -PURPLE='\033[0;35m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# Function to display the menu -show_menu() { - clear - echo -e "${CYAN}================================${NC}" - echo -e "${CYAN} JUNON PROJECT RUNNER${NC}" - echo -e "${CYAN}================================${NC}" - echo "" - echo -e "${YELLOW}Available Operations:${NC}" - echo "" - echo -e "${GREEN}1)${NC} Server (with nodemon & debug)" - echo -e "${GREEN}2)${NC} Client" - echo -e "${GREEN}3)${NC} Server Only (dev mode)" - echo -e "${GREEN}4)${NC} Matchmaker" - echo -e "${GREEN}5)${NC} Client Build" - echo -e "${GREEN}6)${NC} Database Setup" - echo "" - echo -e "${RED}0)${NC} Exit" - echo "" - echo -e "${BLUE}Please select an option (0-6):${NC} " -} - -# Function to run the selected operation -run_operation() { - case $1 in - 1) - echo -e "${GREEN}Starting Server with nodemon and debug...${NC}" - npm run server - ;; - 2) - echo -e "${GREEN}Starting Client...${NC}" - npm run client - ;; - 3) - echo -e "${GREEN}Starting Server Only (dev mode)...${NC}" - npm run serveronly - ;; - 4) - echo -e "${GREEN}Starting Matchmaker...${NC}" - npm run matchmaker - ;; - 5) - echo -e "${GREEN}Building Client...${NC}" - npm run client:build - ;; - 6) - echo -e "${GREEN}Setting up Database...${NC}" - npm run db:setup - ;; - 0) - echo -e "${YELLOW}Goodbye!${NC}" - exit 0 - ;; - *) - echo -e "${RED}Invalid option. Please try again.${NC}" - sleep 2 - return 1 - ;; - esac -} - -# Main loop -while true; do - show_menu - read -r choice - - if run_operation "$choice"; then - echo "" - echo -e "${PURPLE}Operation completed. Press Enter to return to menu...${NC}" - read -r - fi -done \ No newline at end of file From ad7a51a129cae30fe51ba4b22ab3a660943b1cd6 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Tue, 29 Jul 2025 20:53:06 +0800 Subject: [PATCH 06/23] Improved $getBuildingType() I found that when we do $getBuildingType(playername), it didn't return anything, so i fixed it --- packages/junon-io/server/entities/event_handler.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index 4301b01f..731adf4e 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -794,18 +794,23 @@ class EventHandler { } getBuildingType(entityId) { + let player = this.game.getPlayerByName(entityId) + if (player) { + return "Player" + } + let entity = this.game.getEntity(entityId) - + if (!entity) return "" - + if (entity.isPlayer()) { return "Player" } - + if (typeof entity.getTypeName === 'function') { return entity.getTypeName() } - + return entity.type || "" } From ee678d89b2282eb45cd05b1c0dc319c826392d69 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 1 Aug 2025 08:05:44 +0800 Subject: [PATCH 07/23] Be able to directly toggle the modes on or off --- packages/junon-io/server/commands/fly.js | 36 ++++++++--- packages/junon-io/server/commands/god.js | 53 +++++++-------- packages/junon-io/server/commands/spectate.js | 64 ++++++++++++------- 3 files changed, 95 insertions(+), 58 deletions(-) diff --git a/packages/junon-io/server/commands/fly.js b/packages/junon-io/server/commands/fly.js index fee8f1cf..21a5f7d5 100644 --- a/packages/junon-io/server/commands/fly.js +++ b/packages/junon-io/server/commands/fly.js @@ -6,7 +6,9 @@ class Fly extends BaseCommand { getUsage() { return [ "/fly", - "/fly [player]" + "/fly [on|off]", + "/fly [player]", + "/fly [player] [on|off]" ] } @@ -19,21 +21,37 @@ class Fly extends BaseCommand { } perform(player, args) { - const username = args[0] - - let targetPlayers = this.getPlayersBySelector(username) - if (targetPlayers.length === 0) { - if (player.isPlayer()) { - player.toggleFly() + let state = null + if (args.length > 0) { + const lastArg = args[args.length - 1].toLowerCase() + if (lastArg === "on" || lastArg === "off") { + state = lastArg + args = args.slice(0, -1) } + } + + const selector = args[0] + let targetPlayers = this.getPlayersBySelector(selector) + + if (targetPlayers.length === 0 && player.isPlayer()) { + targetPlayers = [player] + } + + if (targetPlayers.length === 0 && selector) { + player.showChatError("no players found") return } - targetPlayers.forEach((targetPlayer) => { - targetPlayer.toggleFly() + targetPlayers.forEach(targetPlayer => { + const shouldEnable = state === null ? !targetPlayer.isFlying : (state === "on") + this.setFly(targetPlayer, shouldEnable) }) } + setFly(player, enabled) { + if (player.isFlying === enabled) return + player.toggleFly() + } } module.exports = Fly \ No newline at end of file diff --git a/packages/junon-io/server/commands/god.js b/packages/junon-io/server/commands/god.js index 58c69eee..123babb3 100644 --- a/packages/junon-io/server/commands/god.js +++ b/packages/junon-io/server/commands/god.js @@ -6,7 +6,9 @@ class God extends BaseCommand { getUsage() { return [ "/god", - "/god [player]" + "/god [on|off]", + "/god [player]", + "/god [player] [on|off]" ] } @@ -20,31 +22,37 @@ class God extends BaseCommand { perform(caller, args) { - const selector = args[0] - if (selector) { - if (!caller.isSectorOwner()) { - caller.showChatError("permission denied") - return + let state = null + if (args.length > 0) { + const lastArg = args[args.length - 1].toLowerCase() + if (lastArg === "on" || lastArg === "off") { + state = lastArg + args = args.slice(0, -1) } + } + + const selector = args[0] + let targetPlayers = this.getPlayersBySelector(selector) - let targetPlayers = this.getPlayersBySelector(selector) - if (targetPlayers.length === 0) { + if (targetPlayers.length === 0 && caller.isPlayer()) { + targetPlayers = [caller] + } + + if (targetPlayers.length === 0 && selector) { caller.showChatError("no players found") return - } - - targetPlayers.forEach((player) => { - this.toggleGod(player) - }) - } else { - if (caller.isPlayer()) { - this.toggleGod(caller) - } } + + targetPlayers.forEach(targetPlayer => { + const shouldEnable = state === null ? !targetPlayer.godMode : (state === "on") + this.setGod(targetPlayer, shouldEnable) + }) } - toggleGod(player) { - player.godMode = !player.godMode + setGod(player, enabled) { + if (player.godMode === enabled) return + + player.godMode = enabled if (player.godMode) { player.setHealth(player.getMaxHealth()) } @@ -53,9 +61,4 @@ class God extends BaseCommand { } } -module.exports = God - - - - - +module.exports = God \ No newline at end of file diff --git a/packages/junon-io/server/commands/spectate.js b/packages/junon-io/server/commands/spectate.js index 38ad31a0..032ef17f 100644 --- a/packages/junon-io/server/commands/spectate.js +++ b/packages/junon-io/server/commands/spectate.js @@ -6,7 +6,9 @@ class Spectate extends BaseCommand { getUsage() { return [ "/spectate", - "/spectate [player]" + "/spectate [on|off]", + "/spectate [player]", + "/spectate [player] [on|off]" ] } @@ -19,43 +21,57 @@ class Spectate extends BaseCommand { } perform(caller, args) { + let state = null + if (args.length > 0) { + const lastArg = args[args.length - 1].toLowerCase() + if (lastArg === "on" || lastArg === "off") { + state = lastArg + args = args.slice(0, -1) + } + } + const selector = args[0] + if (selector) { - if (!caller.isSectorOwner()) { - caller.showChatError("permission denied") + if (!this.game.isPeaceful()) { return } - let targetPlayers = this.getPlayersBySelector(selector) - if (targetPlayers.length === 0) { - caller.showChatError("no players found") + if (!caller.isSectorOwner()) { return } + } - targetPlayers.forEach((player) => { - this.toggleSpectate(player) - }) - } else { - if (caller.isPlayer()) { - this.toggleSpectate(caller) - } + let targetPlayers = this.getPlayersBySelector(selector) + + if (targetPlayers.length === 0 && caller.isPlayer()) { + targetPlayers = [caller] } - + + if (targetPlayers.length === 0 && selector) { + caller.showChatError("no players found") + return + } + + targetPlayers.forEach(targetPlayer => { + const isSpectating = !!targetPlayer.ghost + const shouldEnable = state === null ? !isSpectating : (state === "on") + this.setSpectate(targetPlayer, shouldEnable) + }) } - toggleSpectate(player) { - if (player.ghost) { - player.possess(player) - player.showChatSuccess("spectate mode: OFF") - } else { + setSpectate(player, enabled) { + const isSpectating = !!player.ghost + if (isSpectating === enabled) return + + if (enabled) { player.transformIntoGhost() player.showChatSuccess("spectate mode: ON") + } else { + player.possess(player) + player.showChatSuccess("spectate mode: OFF") } } } -module.exports = Spectate - - - - +module.exports = Spectate \ No newline at end of file From 409223c5dd02b304dd852ffb95e209b19d1ce857 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 1 Aug 2025 08:31:56 +0800 Subject: [PATCH 08/23] improved spectate.js --- packages/junon-io/server/commands/spectate.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/junon-io/server/commands/spectate.js b/packages/junon-io/server/commands/spectate.js index 032ef17f..deff7c00 100644 --- a/packages/junon-io/server/commands/spectate.js +++ b/packages/junon-io/server/commands/spectate.js @@ -26,18 +26,19 @@ class Spectate extends BaseCommand { const lastArg = args[args.length - 1].toLowerCase() if (lastArg === "on" || lastArg === "off") { state = lastArg - args = args.slice(0, -1) + args = args.slice(0, -1) } } const selector = args[0] if (selector) { - if (!this.game.isPeaceful()) { + if (!caller.isAdminMode && !this.game.isPeaceful()) { return } - if (!caller.isSectorOwner()) { + const hasPermission = caller.isAdminMode || caller.isSectorOwner() || caller.hasCommandsPermission() + if (!hasPermission) { return } } From c6da7d265add8f67aa4afd7ac48d402cb913f148 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 1 Aug 2025 09:41:59 +0800 Subject: [PATCH 09/23] improved the "raid" flag i added in /spawnmob and fixed original raid event mobs never despawn bug --- .../junon-io/server/commands/spawn_mob.js | 38 ++++++++++++------- packages/junon-io/server/entities/raid.js | 3 -- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/junon-io/server/commands/spawn_mob.js b/packages/junon-io/server/commands/spawn_mob.js index 56b00ffe..0ae53058 100644 --- a/packages/junon-io/server/commands/spawn_mob.js +++ b/packages/junon-io/server/commands/spawn_mob.js @@ -149,19 +149,29 @@ class SpawnMob extends BaseCommand { if (keyValueMap["raid"] === "true") { const Raid = require("../entities/raid") - const fakeEventManager = this.game.eventManager - const raidData = { - sector: this.sector, - team: caller.getTeam ? caller.getTeam() : undefined, - permanent: true + const eventManager = this.game.eventManager + let permanentRaid = null + + if (eventManager.raids && Array.isArray(eventManager.raids)) { + permanentRaid = eventManager.raids.find(raid => raid.data && raid.data.permanent === true) + } + + if (!permanentRaid) { + const raidData = { + sector: this.sector, + team: caller.getTeam ? caller.getTeam() : undefined, + permanent: true + } + permanentRaid = new Raid(eventManager, raidData) + permanentRaid.prepare() + } + + if (permanentRaid) { + mobs.forEach((mob) => { + mob.setRaid(permanentRaid) + permanentRaid.addToMobGroup(mob) + }) } - const raid = new Raid(fakeEventManager, raidData) - raid.prepare() - if (!fakeEventManager.raids) fakeEventManager.raids = [] - fakeEventManager.raids.push(raid) - mobs.forEach((mob) => { - mob.setRaid(raid) - }) } mobs.forEach((mob) => { @@ -176,7 +186,7 @@ class SpawnMob extends BaseCommand { } }) - if (mobs.length > 1) { + if (mobs.length > 1 && keyValueMap["raid"] !== "true") { let entityGroup = new EntityGroup() mobs.forEach((mob) => { entityGroup.addChild(mob) @@ -185,4 +195,4 @@ class SpawnMob extends BaseCommand { } } -module.exports = SpawnMob +module.exports = SpawnMob \ No newline at end of file diff --git a/packages/junon-io/server/entities/raid.js b/packages/junon-io/server/entities/raid.js index 31b30aec..d636884c 100644 --- a/packages/junon-io/server/entities/raid.js +++ b/packages/junon-io/server/entities/raid.js @@ -504,9 +504,6 @@ class Raid { } endRaid() { - // mobs with "raid:true" flag in /spawnmob will never end (it has bug in sandbox mode after you spawn mobs with raid:true flag, raid event mob will never despawn) - if (this.data && this.data.permanent) return - if (this.isRaidEnded) { if (this.isRaidTooLong()) { // if i still have mobs alive, remove them immediately From d323eb14f94130936227183c5bb54377f11b974a Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 1 Aug 2025 18:55:52 +0800 Subject: [PATCH 10/23] found a bug in /spawnmob raid flag and fixed --- packages/junon-io/server/entities/raid.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/junon-io/server/entities/raid.js b/packages/junon-io/server/entities/raid.js index d636884c..070ef654 100644 --- a/packages/junon-io/server/entities/raid.js +++ b/packages/junon-io/server/entities/raid.js @@ -488,6 +488,7 @@ class Raid { } onRaidGoalTargetRemoved() { + if (this.data && this.data.permanent) return if (this.game.isHardcore()) return if (this.boss) return From 9c385364da69c662d6577c1c84cd0e7c90968073 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 1 Aug 2025 20:50:25 +0800 Subject: [PATCH 11/23] improved raid flag agian... --- packages/junon-io/server/entities/raid.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/junon-io/server/entities/raid.js b/packages/junon-io/server/entities/raid.js index 070ef654..6a980032 100644 --- a/packages/junon-io/server/entities/raid.js +++ b/packages/junon-io/server/entities/raid.js @@ -5,6 +5,7 @@ const EventBus = require('eventbusjs') class Raid { constructor(eventManager, data) { + this.data = data this.eventManager = eventManager this.game = eventManager.game @@ -97,8 +98,6 @@ class Raid { this.team = this.game.getEntity(data.team.id) } - if (!this.team) return - if (data.spawnGroundRow) { let ground = this.sector.groundMap.get(data.spawnGroundRow, data.spawnGroundCol) if (!ground) { @@ -116,11 +115,12 @@ class Raid { this.occurTimestamp = this.game.timestamp + (Constants.physicsTimeStep * Constants.secondsPerHour) } - - let structures = this.team.getRaidableOwnedStructures() - this.structureCount = structures.length - - if (structures.length === 0) return + if (this.team) { + let structures = this.team.getRaidableOwnedStructures() + this.structureCount = structures.length + } else { + this.structureCount = 0 + } } determineSpawnRoom(team) { @@ -586,4 +586,4 @@ class Raid { } -module.exports = Raid +module.exports = Raid \ No newline at end of file From fc7a94f4ac7c5111c1e7bf9a63549548173b7006 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 09:28:11 +0800 Subject: [PATCH 12/23] merging --- packages/junon-io/client/main.ejs | 4 +-- .../junon-io/server/entities/event_handler.js | 28 ++++--------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 5de7b128..81b023b5 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -1051,8 +1051,8 @@ $getAngle - $entityId - Gets the current facing angle of an entity in degrees. + $player + Returns the angle of the specified player. Ex. /chat @a $getAngle($player) diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index 731adf4e..b7c5c340 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -259,22 +259,10 @@ class EventHandler { return this._fixFloat(Math.atan(num) * 180 / Math.PI) } - getAngle(entityId) { - let player = this.getPlayer(entityId) - let angle = 0 - - if (player && typeof player.angle === 'number') { - angle = player.angle - } else { - let entity = this.game.getEntity(entityId) - if (entity && typeof entity.angle === 'number') { - angle = entity.angle - } - } - - //Normalize angle to 0-360 degrees, if this is not added, the angle can be greater than 360, idk why. - angle = ((angle % 360) + 360) % 360 - return angle + getAngle(playerId) { + let player = this.getPlayer(playerId) + if (!player) return 0 + return player.angle } length(value) { @@ -1249,13 +1237,7 @@ class EventHandler { "$getStructureByCoords": true, "$hasEffect": true, "$getTotalMobCount": true, - "$getAngle": true, - "$sin": true, - "$cos": true, - "$tan": true, - "$asin": true, - "$acos": true, - "$atan": true, + "$getAngle": true } } From 935a44fe09f155729006e52ef38b4b92fe40623d Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 09:41:40 +0800 Subject: [PATCH 13/23] Added my $getAngle back --- packages/junon-io/client/main.ejs | 4 +-- .../junon-io/server/entities/event_handler.js | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 81b023b5..5de7b128 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -1051,8 +1051,8 @@ $getAngle - $player - Returns the angle of the specified player. Ex. /chat @a $getAngle($player) + $entityId + Gets the current facing angle of an entity in degrees. diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index b7c5c340..c8f086f5 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -259,10 +259,21 @@ class EventHandler { return this._fixFloat(Math.atan(num) * 180 / Math.PI) } - getAngle(playerId) { - let player = this.getPlayer(playerId) - if (!player) return 0 - return player.angle + getAngle(entityId) { + let player = this.getPlayer(entityId) + let angle = 0 + + if (player && typeof player.angle === 'number') { + angle = player.angle + } else { + let entity = this.game.getEntity(entityId) + if (entity && typeof entity.angle === 'number') { + angle = entity.angle + } + } + + angle = ((angle % 360) + 360) % 360 + return angle } length(value) { @@ -1237,7 +1248,13 @@ class EventHandler { "$getStructureByCoords": true, "$hasEffect": true, "$getTotalMobCount": true, - "$getAngle": true + "$getAngle": true, + "$sin": true, + "$cos": true, + "$tan": true, + "$asin": true, + "$acos": true, + "$atan": true, } } From 6b1b43655d2ebeb2154e05394d02ec17b6b4921e Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 09:46:06 +0800 Subject: [PATCH 14/23] Revert "Added my $getAngle back" This reverts commit 935a44fe09f155729006e52ef38b4b92fe40623d. --- packages/junon-io/client/main.ejs | 4 +-- .../junon-io/server/entities/event_handler.js | 27 ++++--------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 5de7b128..81b023b5 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -1051,8 +1051,8 @@ $getAngle - $entityId - Gets the current facing angle of an entity in degrees. + $player + Returns the angle of the specified player. Ex. /chat @a $getAngle($player) diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index c8f086f5..b7c5c340 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -259,21 +259,10 @@ class EventHandler { return this._fixFloat(Math.atan(num) * 180 / Math.PI) } - getAngle(entityId) { - let player = this.getPlayer(entityId) - let angle = 0 - - if (player && typeof player.angle === 'number') { - angle = player.angle - } else { - let entity = this.game.getEntity(entityId) - if (entity && typeof entity.angle === 'number') { - angle = entity.angle - } - } - - angle = ((angle % 360) + 360) % 360 - return angle + getAngle(playerId) { + let player = this.getPlayer(playerId) + if (!player) return 0 + return player.angle } length(value) { @@ -1248,13 +1237,7 @@ class EventHandler { "$getStructureByCoords": true, "$hasEffect": true, "$getTotalMobCount": true, - "$getAngle": true, - "$sin": true, - "$cos": true, - "$tan": true, - "$asin": true, - "$acos": true, - "$atan": true, + "$getAngle": true } } From 3590deaee8a0d56c1ec9717d63da5f56697fe17d Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 11:39:18 +0800 Subject: [PATCH 15/23] added my $getAngle back again... --- packages/junon-io/client/main.ejs | 4 +-- .../junon-io/server/entities/event_handler.js | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 81b023b5..5de7b128 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -1051,8 +1051,8 @@ $getAngle - $player - Returns the angle of the specified player. Ex. /chat @a $getAngle($player) + $entityId + Gets the current facing angle of an entity in degrees. diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index baa72232..3161228b 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -259,10 +259,21 @@ class EventHandler { return this._fixFloat(Math.atan(num) * 180 / Math.PI) } - getAngle(playerId) { - let player = this.getPlayer(playerId) - if (!player) return 0 - return player.angle + getAngle(entityId) { + let player = this.getPlayer(entityId) + let angle = 0 + + if (player && typeof player.angle === 'number') { + angle = player.angle + } else { + let entity = this.game.getEntity(entityId) + if (entity && typeof entity.angle === 'number') { + angle = entity.angle + } + } + + angle = ((angle % 360) + 360) % 360 + return angle } length(value) { @@ -1243,7 +1254,13 @@ class EventHandler { "$getStructureByCoords": true, "$hasEffect": true, "$getTotalMobCount": true, - "$getAngle": true + "$getAngle": true, + "$sin": true, + "$cos": true, + "$tan": true, + "$asin": true, + "$acos": true, + "$atan": true, } } From 1d6e669e1b9e11a196d7b27cf7b095caa17852a7 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 19:22:39 +0800 Subject: [PATCH 16/23] added "owner" flag in /projectile --- .../junon-io/server/commands/projectile.js | 26 ++++++++++++++++--- .../server/entities/projectiles/grenade.js | 1 + packages/junon-io/server/entities/sector.js | 5 +++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/junon-io/server/commands/projectile.js b/packages/junon-io/server/commands/projectile.js index e565b92a..977583de 100644 --- a/packages/junon-io/server/commands/projectile.js +++ b/packages/junon-io/server/commands/projectile.js @@ -23,9 +23,23 @@ class Projectile extends BaseCommand { const type = args[0] const row = parseInt(args[1]) const col = parseInt(args[2]) - let keyValueArgs = isNaN(row) ? args.slice(3) : args.slice(1) + let keyValueArgs = isNaN(row) ? args.slice(1) : args.slice(3) let keyValueMap = this.convertKeyValueArgsToObj(keyValueArgs) + let owner = caller + + if (keyValueMap.owner) { + const entityId = parseInt(keyValueMap.owner, 10) + + if (!isNaN(entityId)) { + const foundOwner = this.game.getEntity(entityId) + + if (foundOwner) { + owner = foundOwner + } + } + } + let x, y if (col) { @@ -42,6 +56,12 @@ class Projectile extends BaseCommand { if (typeof x === 'undefined' || typeof y === 'undefined') return + const baseSpawnConfig = { + owner: owner, + type: type, + keyValueMap: keyValueMap + }; + if (keyValueMap["scatter"]) { let scatterCount = 10 let rowSpread = 8 @@ -50,10 +70,10 @@ class Projectile extends BaseCommand { let deltaCol = rowSpread - Math.floor(Math.random() * rowSpread) * 2 let otherX = x + deltaCol * Constants.tileSize + Constants.tileSize/2 let otherY = y + deltaRow * Constants.tileSize + Constants.tileSize/2 - this.sector.spawnProjectile({ caller: caller, type: type, x: otherX, y: otherY, keyValueMap: keyValueMap }) + this.sector.spawnProjectile({ ...baseSpawnConfig, caller: caller, x: otherX, y: otherY }) } } else { - this.sector.spawnProjectile({ caller: caller, type: type, x: x, y: y, keyValueMap: keyValueMap }) + this.sector.spawnProjectile({ ...baseSpawnConfig, caller: caller, x: x, y: y }) } } diff --git a/packages/junon-io/server/entities/projectiles/grenade.js b/packages/junon-io/server/entities/projectiles/grenade.js index f32ec891..63c954e3 100644 --- a/packages/junon-io/server/entities/projectiles/grenade.js +++ b/packages/junon-io/server/entities/projectiles/grenade.js @@ -60,6 +60,7 @@ class Grenade extends BaseProjectile { createExplosion() { return this.sector.createProjectile("Explosion", { + owner: this.getOwner(), weapon: this.weapon, source: { x: this.getX(), y: this.getY() }, destination: { x: this.getX(), y: this.getY() } diff --git a/packages/junon-io/server/entities/sector.js b/packages/junon-io/server/entities/sector.js index 2a97d81b..e253721c 100644 --- a/packages/junon-io/server/entities/sector.js +++ b/packages/junon-io/server/entities/sector.js @@ -2746,7 +2746,10 @@ class Sector { return false } - options.owner = this + if (!options.owner) { + options.owner = this + } + options.source = { x: x, y: y} options.destination = { x: x, y: y} klass.build(options) From 525be1b4d938236b6777103e370c269c56e662c4 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 21:19:18 +0800 Subject: [PATCH 17/23] removed isCollidable() in some .js cause mobs can damage them now, so they are no longer needed --- packages/junon-io/server/entities/buildings/blue_present.js | 5 ----- packages/junon-io/server/entities/buildings/chair.js | 5 ----- packages/junon-io/server/entities/buildings/green_present.js | 5 ----- packages/junon-io/server/entities/buildings/lamp.js | 5 ----- packages/junon-io/server/entities/buildings/rail_stop.js | 4 ---- packages/junon-io/server/entities/buildings/red_present.js | 5 ----- packages/junon-io/server/entities/buildings/sign.js | 5 ----- 7 files changed, 34 deletions(-) diff --git a/packages/junon-io/server/entities/buildings/blue_present.js b/packages/junon-io/server/entities/buildings/blue_present.js index 9d994bc5..0f0b01c6 100644 --- a/packages/junon-io/server/entities/buildings/blue_present.js +++ b/packages/junon-io/server/entities/buildings/blue_present.js @@ -11,11 +11,6 @@ class BluePresent extends BaseBuilding { getType() { return Protocol.definition().BuildingType.BluePresent } - - isCollidable() { - return false - } - } module.exports = BluePresent diff --git a/packages/junon-io/server/entities/buildings/chair.js b/packages/junon-io/server/entities/buildings/chair.js index ead378a0..37803110 100644 --- a/packages/junon-io/server/entities/buildings/chair.js +++ b/packages/junon-io/server/entities/buildings/chair.js @@ -11,11 +11,6 @@ class Chair extends BaseBuilding { getType() { return Protocol.definition().BuildingType.Chair } - - isCollidable() { - return false - } - } module.exports = Chair diff --git a/packages/junon-io/server/entities/buildings/green_present.js b/packages/junon-io/server/entities/buildings/green_present.js index 94d92b63..3b179ab6 100644 --- a/packages/junon-io/server/entities/buildings/green_present.js +++ b/packages/junon-io/server/entities/buildings/green_present.js @@ -11,11 +11,6 @@ class GreenPresent extends BaseBuilding { getType() { return Protocol.definition().BuildingType.GreenPresent } - - isCollidable() { - return false - } - } module.exports = GreenPresent diff --git a/packages/junon-io/server/entities/buildings/lamp.js b/packages/junon-io/server/entities/buildings/lamp.js index b7cfcc43..0b0911a5 100644 --- a/packages/junon-io/server/entities/buildings/lamp.js +++ b/packages/junon-io/server/entities/buildings/lamp.js @@ -49,11 +49,6 @@ class Lamp extends BaseBuilding { getType() { return Protocol.definition().BuildingType.Lamp } - - isCollidable() { - return false - } - } diff --git a/packages/junon-io/server/entities/buildings/rail_stop.js b/packages/junon-io/server/entities/buildings/rail_stop.js index fe04a346..8659ca16 100644 --- a/packages/junon-io/server/entities/buildings/rail_stop.js +++ b/packages/junon-io/server/entities/buildings/rail_stop.js @@ -261,10 +261,6 @@ class RailStop extends BaseBuilding { super.breakBuilding(lastBreaker) } - isCollidable() { - return false - } - shouldObstruct(body, hit) { let box = this.getCollisionBox() diff --git a/packages/junon-io/server/entities/buildings/red_present.js b/packages/junon-io/server/entities/buildings/red_present.js index cf48073f..d0f6bbb6 100644 --- a/packages/junon-io/server/entities/buildings/red_present.js +++ b/packages/junon-io/server/entities/buildings/red_present.js @@ -11,11 +11,6 @@ class RedPresent extends BaseBuilding { getType() { return Protocol.definition().BuildingType.RedPresent } - - isCollidable() { - return false - } - } module.exports = RedPresent diff --git a/packages/junon-io/server/entities/buildings/sign.js b/packages/junon-io/server/entities/buildings/sign.js index 3ae144f9..8a9ae30f 100644 --- a/packages/junon-io/server/entities/buildings/sign.js +++ b/packages/junon-io/server/entities/buildings/sign.js @@ -17,11 +17,6 @@ class Sign extends BaseBuilding { getType() { return Protocol.definition().BuildingType.Sign } - - isCollidable() { - return false - } - } module.exports = Sign From ee8c23f239a034b28db601216bab45a370612bbe Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Sun, 3 Aug 2025 22:51:12 +0800 Subject: [PATCH 18/23] make melee mobs can damage walls/ custom damage for guards and pirates and sth else --- packages/junon-io/common/constants.json | 8 ++ .../entities/buildings/base_building.js | 22 +++++ .../equipments/hand/hand_equipment.js | 20 ++-- .../equipments/hand/melee_equipment.js | 29 +++--- .../junon-io/server/entities/mobs/base_mob.js | 91 +++++++++++++++---- .../junon-io/server/entities/mobs/chemist.js | 4 + .../junon-io/server/entities/mobs/firebat.js | 10 +- .../junon-io/server/entities/mobs/pirate.js | 6 -- 8 files changed, 139 insertions(+), 51 deletions(-) diff --git a/packages/junon-io/common/constants.json b/packages/junon-io/common/constants.json index 9cc3fd55..c8d1f571 100644 --- a/packages/junon-io/common/constants.json +++ b/packages/junon-io/common/constants.json @@ -985,6 +985,7 @@ "gold": 2000 }, "isBuildingDestroyer": true, + "canDamageWalls": true, "attackGroups": ["players", "buildings", "mobs"], "drop": ["Web"], "resistance": ["fire"], @@ -1044,6 +1045,8 @@ "cost": { "gold": 500 }, + "isBuildingDestroyer": true, + "canDamageWalls": true, "attackGroups": ["players", "buildings", "mobs", "distributions"], "sprite": { "width": 40, @@ -1121,6 +1124,7 @@ }, "Sapper": { "parent": "Mobs.Guard", + "resistance": ["fire"], "sprite": { "width": 60, "height": 181 @@ -1143,6 +1147,8 @@ "cost": { "gold": 2500 }, + "isBuildingDestroyer": true, + "canDamageWalls": true, "attackGroups": ["players", "buildings", "mobs"], "sprite": { "width": 50, @@ -1168,6 +1174,8 @@ "Firebat": { "parent": "Mobs.BaseMob", "hasBlood": true, + "isBuildingDestroyer": true, + "canDamageWalls": true, "drop": ["Flamethrower"], "resistance": ["fire"], "immunity": ["poison"], diff --git a/packages/junon-io/server/entities/buildings/base_building.js b/packages/junon-io/server/entities/buildings/base_building.js index 60f6e89f..ffeb8c24 100644 --- a/packages/junon-io/server/entities/buildings/base_building.js +++ b/packages/junon-io/server/entities/buildings/base_building.js @@ -1930,6 +1930,28 @@ Object.assign(BaseBuilding.prototype, Upgradable.prototype, { }) Object.assign(BaseBuilding.prototype, Destroyable.prototype, { + damage(amount, attacker) { + if (!this.isDestroyable() || this.godMode) return; + if (attacker && attacker.isCollidable && !this.isCollidable(attacker)) return; + amount -= this.getDamageResistance(amount, attacker); + if (amount <= 0) return; + this.reduceHealth(amount); + this.onDamaged(attacker, amount); + }, + + getDamageResistance(amount, attacker) { + if (attacker && attacker.hasCategory && attacker.hasCategory("fire") && this.isResistantTo("fire")) { + return Math.floor(amount / 2); + } + + const isMeleeAttack = attacker && typeof attacker.hasMeleeWeapon === 'function' && attacker.hasMeleeWeapon(); + if (this.hasCategory("melee_resistant") && isMeleeAttack) { + return Math.floor(amount * 0.5); + } + + return 0; +}, + onDamaged(attacker, amount) { let data = { entityId: this.getId(), diff --git a/packages/junon-io/server/entities/equipments/hand/hand_equipment.js b/packages/junon-io/server/entities/equipments/hand/hand_equipment.js index 7555b1dd..c9389186 100644 --- a/packages/junon-io/server/entities/equipments/hand/hand_equipment.js +++ b/packages/junon-io/server/entities/equipments/hand/hand_equipment.js @@ -34,22 +34,14 @@ class HandEquipment extends BaseEquipment { } getDamage(targetEntity) { - let baseDamage = this.getEquipmentDamage() - if (!this.game.isMiniGame() && - targetEntity && - targetEntity.hasCategory("melee_resistant")) { - baseDamage = 2 - } - - if (!this.owner) return baseDamage - - if (this.owner.isMob() || this.owner.isPlayer()) { - return Math.floor(this.owner.getDamageMultiplier() * baseDamage) - } else { - return baseDamage - } + const customStats = this.sector.entityCustomStats[this.item.id]; + if (customStats && typeof customStats.damage !== 'undefined') { + return customStats.damage; } + return this.getEquipmentDamage() || 0; +} + getEquipmentDamage() { if (this.sector) { if (this.sector.entityCustomStats[this.item.id]) { diff --git a/packages/junon-io/server/entities/equipments/hand/melee_equipment.js b/packages/junon-io/server/entities/equipments/hand/melee_equipment.js index b72e1838..f85cf371 100644 --- a/packages/junon-io/server/entities/equipments/hand/melee_equipment.js +++ b/packages/junon-io/server/entities/equipments/hand/melee_equipment.js @@ -40,7 +40,13 @@ class MeleeEquipment extends HandEquipment { success = true } else { - let target = user.getMeleeTarget(this.getMeleeRange(), meleeTargetOptions) //this.getMeleeTarget(user) + let target + if (user.isMob && user.isMob()) { + target = targetEntity + } else { + target = user.getMeleeTarget(this.getMeleeRange(), meleeTargetOptions) + } + success = this.useOnTarget(user, target) } @@ -86,20 +92,21 @@ class MeleeEquipment extends HandEquipment { } useOnTarget(user, target) { - const damage = this.getDamage(target) - if (target) { - target.damage(damage, user, this) - if (user.isPlayer()) { - } - - if (this.canStunEnemy()) { - this.applyStun(target) - } + const damage = user.getDamage(target); + if (target) { + target.damage(damage, user, this); + + if (user.isPlayer()) { } - return true + if (this.canStunEnemy()) { + this.applyStun(target); + } } + return true; +} + applyStun(target) { let knockChance = 1 //0.15 if (Math.random() < knockChance) { diff --git a/packages/junon-io/server/entities/mobs/base_mob.js b/packages/junon-io/server/entities/mobs/base_mob.js index d4cbc13a..d1151120 100644 --- a/packages/junon-io/server/entities/mobs/base_mob.js +++ b/packages/junon-io/server/entities/mobs/base_mob.js @@ -2147,6 +2147,39 @@ class BaseMob extends BaseEntity { return false } + getWeapon() { + if (this.equipments && typeof this.equipments.get === 'function') { + const item = this.equipments.get(Protocol.definition().EquipmentRole.Hand); + return item ? item.instance : null; + } + return null; +} + +getDamage(attackTarget) { + const weapon = this.getWeapon(); + let totalDamage; + + if (weapon) { + const baseDamage = super.getDamage(); + const weaponDamage = weapon.getDamage(attackTarget); + totalDamage = baseDamage + weaponDamage; + } else { + totalDamage = super.getDamage(); + } + + // 處理自訂傷害覆蓋 (此處的自訂傷害是針對 Mob 本身) + if (this.sector) { + if (this.sector.entityCustomStats[this.id] && typeof this.sector.entityCustomStats[this.id].damage !== 'undefined') { + totalDamage = this.sector.entityCustomStats[this.id].damage; + } else if (this.sector.mobCustomStats[this.type] && typeof this.sector.mobCustomStats[this.type].damage !== 'undefined') { + totalDamage = this.sector.mobCustomStats[this.type].damage; + } + } + + const damageMultiplier = this.getDamageMultiplier(attackTarget); + return Math.floor(totalDamage * damageMultiplier); +} + getRotatedAngle() { return this.angle } @@ -2162,21 +2195,34 @@ class BaseMob extends BaseEntity { } getDamage(attackTarget) { - let damage = super.getDamage() - - if (this.sector) { - if (this.sector.entityCustomStats[this.id]) { - damage = this.sector.entityCustomStats[this.id].damage - } else if (this.sector.mobCustomStats[this.type]) { - damage = this.sector.mobCustomStats[this.type].damage - } - } + const weapon = this.getWeapon(); + let totalDamage; - let damageMultiplier = this.getDamageMultiplier(attackTarget) + // 檢查 mob 是否裝備了武器 + if (weapon) { + // 如果有武器,則傷害 = mob 基礎傷害 + 武器傷害 + const baseDamage = super.getDamage(); // 取得 mob 在 constants.json 中設定的空手傷害 + const weaponDamage = weapon.getDamage(attackTarget); // 取得武器自身的傷害 + totalDamage = baseDamage + weaponDamage; + } else { + // 如果沒有武器,則只使用 mob 的基礎傷害 + totalDamage = super.getDamage(); + } - return Math.floor(damageMultiplier * damage) + // 檢查是否有來自伺服器設定的自訂傷害值,若有則覆蓋計算結果 (保留原有邏輯) + if (this.sector) { + if (this.sector.entityCustomStats[this.id] && typeof this.sector.entityCustomStats[this.id].damage !== 'undefined') { + totalDamage = this.sector.entityCustomStats[this.id].damage; + } else if (this.sector.mobCustomStats[this.type] && typeof this.sector.mobCustomStats[this.type].damage !== 'undefined') { + totalDamage = this.sector.mobCustomStats[this.type].damage; + } } + // 最後,應用傷害乘數 (例如對建築物的加成) + const damageMultiplier = this.getDamageMultiplier(attackTarget); + return Math.floor(damageMultiplier * totalDamage); +} + getResistance() { return this.getConstants().resistance || [] } @@ -2278,13 +2324,24 @@ Object.assign(BaseMob.prototype, Movable.prototype, { Object.assign(BaseMob.prototype, Destroyable.prototype, { - getDamageResistance(amount, attackEntity) { - if (attackEntity.hasCategory("fire") && this.isResistantTo("fire")) { - return Math.floor(amount / 2) - } + damage(amount, attacker) { + if (!this.isDestroyable() || this.godMode || this.isInvincible) return; + amount -= this.getDamageResistance(amount, attacker); + if (amount <= 0) return; + this.reduceHealth(amount); + this.onDamaged(attacker, amount); +}, + + getDamageResistance(amount, attacker) { + const isMeleeAttack = attacker && typeof attacker.hasMeleeWeapon === 'function' && attacker.hasMeleeWeapon(); + + if (this.hasCategory("melee_resistant") && isMeleeAttack) { + return Math.floor(amount * 0.5); + } + + return 0; +}, - return 0 - }, onDamaged(attacker, amount) { this.attackerId = attacker.id diff --git a/packages/junon-io/server/entities/mobs/chemist.js b/packages/junon-io/server/entities/mobs/chemist.js index 1a290f6d..9ccec041 100644 --- a/packages/junon-io/server/entities/mobs/chemist.js +++ b/packages/junon-io/server/entities/mobs/chemist.js @@ -150,6 +150,10 @@ class Chemist extends LandMob { } getWeapon() { + if (!this.equipments) { + return null; + } + let item = this.equipments.get(Protocol.definition().EquipmentRole.Hand) if (!item) return null return item.instance || item.getKlass(item.type) diff --git a/packages/junon-io/server/entities/mobs/firebat.js b/packages/junon-io/server/entities/mobs/firebat.js index 1e829543..3fee84bb 100644 --- a/packages/junon-io/server/entities/mobs/firebat.js +++ b/packages/junon-io/server/entities/mobs/firebat.js @@ -124,11 +124,15 @@ class Firebat extends LandMob { } getWeapon() { - let item = this.equipments.get(Protocol.definition().EquipmentRole.Hand) - if (!item) return null - return item.instance || item.getKlass(item.type) + if (!this.equipments) { + return null; } + let item = this.equipments.get(Protocol.definition().EquipmentRole.Hand) + if (!item) return null + return item.instance || item.getKlass(item.type) +} + remove() { super.remove() diff --git a/packages/junon-io/server/entities/mobs/pirate.js b/packages/junon-io/server/entities/mobs/pirate.js index b6fef681..58a4e152 100644 --- a/packages/junon-io/server/entities/mobs/pirate.js +++ b/packages/junon-io/server/entities/mobs/pirate.js @@ -102,12 +102,6 @@ class Pirate extends LandMob { } } - getWeapon() { - let item = this.equipments.get(Protocol.definition().EquipmentRole.Hand) - if (!item) return null - return item.instance - } - remove() { super.remove() From ddd2d078cd896219b381af42e70131497ba9a5eb Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Mon, 4 Aug 2025 12:44:08 +0800 Subject: [PATCH 19/23] fix the bug that some range mobs can't damage walls --- .../junon-io/server/entities/buildings/base_building.js | 6 +----- .../server/entities/equipments/hand/hand_equipment.js | 1 - packages/junon-io/server/entities/mobs/base_mob.js | 1 - .../server/entities/projectiles/base_projectile.js | 8 ++++++++ .../server/entities/projectiles/collidable_projectile.js | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/junon-io/server/entities/buildings/base_building.js b/packages/junon-io/server/entities/buildings/base_building.js index ffeb8c24..6f71bf4f 100644 --- a/packages/junon-io/server/entities/buildings/base_building.js +++ b/packages/junon-io/server/entities/buildings/base_building.js @@ -1940,16 +1940,12 @@ Object.assign(BaseBuilding.prototype, Destroyable.prototype, { }, getDamageResistance(amount, attacker) { - if (attacker && attacker.hasCategory && attacker.hasCategory("fire") && this.isResistantTo("fire")) { - return Math.floor(amount / 2); - } - const isMeleeAttack = attacker && typeof attacker.hasMeleeWeapon === 'function' && attacker.hasMeleeWeapon(); if (this.hasCategory("melee_resistant") && isMeleeAttack) { return Math.floor(amount * 0.5); } - return 0; + return 0; }, onDamaged(attacker, amount) { diff --git a/packages/junon-io/server/entities/equipments/hand/hand_equipment.js b/packages/junon-io/server/entities/equipments/hand/hand_equipment.js index c9389186..c99599c6 100644 --- a/packages/junon-io/server/entities/equipments/hand/hand_equipment.js +++ b/packages/junon-io/server/entities/equipments/hand/hand_equipment.js @@ -38,7 +38,6 @@ class HandEquipment extends BaseEquipment { if (customStats && typeof customStats.damage !== 'undefined') { return customStats.damage; } - return this.getEquipmentDamage() || 0; } diff --git a/packages/junon-io/server/entities/mobs/base_mob.js b/packages/junon-io/server/entities/mobs/base_mob.js index d1151120..a2fe972d 100644 --- a/packages/junon-io/server/entities/mobs/base_mob.js +++ b/packages/junon-io/server/entities/mobs/base_mob.js @@ -2158,7 +2158,6 @@ class BaseMob extends BaseEntity { getDamage(attackTarget) { const weapon = this.getWeapon(); let totalDamage; - if (weapon) { const baseDamage = super.getDamage(); const weaponDamage = weapon.getDamage(attackTarget); diff --git a/packages/junon-io/server/entities/projectiles/base_projectile.js b/packages/junon-io/server/entities/projectiles/base_projectile.js index 6e26e755..543442c1 100644 --- a/packages/junon-io/server/entities/projectiles/base_projectile.js +++ b/packages/junon-io/server/entities/projectiles/base_projectile.js @@ -276,6 +276,14 @@ class BaseProjectile extends BaseEntity { this.remove() } + canDamageWalls() { + if (this.sourceEntity && typeof this.sourceEntity.canDamageWalls === 'function') { + return this.sourceEntity.canDamageWalls(); + } + + return this.getConstants().canDamageWalls; + } + canDamage(entity) { if(this.sourceEntity && this.sourceEntity.canAttack) return this.sourceEntity.canAttack(entity) /* if can/can't attack, then can/can't damage. diff --git a/packages/junon-io/server/entities/projectiles/collidable_projectile.js b/packages/junon-io/server/entities/projectiles/collidable_projectile.js index 6fba25c3..2a586e91 100644 --- a/packages/junon-io/server/entities/projectiles/collidable_projectile.js +++ b/packages/junon-io/server/entities/projectiles/collidable_projectile.js @@ -133,8 +133,8 @@ class CollidableProjectile extends BaseProjectile { } getCollidables() { - return [this.sector.playerTree, this.sector.mobTree, this.sector.structureMap] - } + return [this.sector.playerTree, this.sector.mobTree, this.sector.structureMap, this.sector.armorMap] +} } From 5f060e615563efa9e2f270d7cee0510b787aa8e2 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Mon, 4 Aug 2025 13:34:59 +0800 Subject: [PATCH 20/23] fixed the bug that attackerId works incorrectly --- packages/junon-io/common/constants.json | 1 + .../junon-io/server/entities/base_entity.js | 31 ++++++++++++------- packages/junon-io/server/entities/player.js | 24 ++++++++++++-- .../entities/projectiles/base_projectile.js | 4 +-- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/junon-io/common/constants.json b/packages/junon-io/common/constants.json index c8d1f571..26870e86 100644 --- a/packages/junon-io/common/constants.json +++ b/packages/junon-io/common/constants.json @@ -1125,6 +1125,7 @@ "Sapper": { "parent": "Mobs.Guard", "resistance": ["fire"], + "immunity": ["poison"], "sprite": { "width": 60, "height": 181 diff --git a/packages/junon-io/server/entities/base_entity.js b/packages/junon-io/server/entities/base_entity.js index 2af70ec3..88dbec64 100644 --- a/packages/junon-io/server/entities/base_entity.js +++ b/packages/junon-io/server/entities/base_entity.js @@ -1737,22 +1737,29 @@ class BaseEntity extends BaseTransientEntity { } getKillerFromAttacker(attacker) { - if (attacker.isMob()) return attacker - if (attacker.isPlayer()) return attacker - if (typeof attacker.isProjectile !== 'function') return null - - if (attacker.isProjectile()) { - if (attacker.weapon) { - if (attacker.weapon.isBuilding()) { - return attacker.weapon.placer - } else { - // weapon equipment - return attacker.owner - } + if (attacker.isMob && attacker.isMob() || attacker.isPlayer && attacker.isPlayer()) { + return attacker; + } + + if (attacker.isBuilding && attacker.isBuilding()) { + return attacker; + } + + if (attacker.isProjectile && attacker.isProjectile()) { + const shooter = attacker.sourceEntity; + + if (shooter) { + if (shooter.isBuilding && shooter.isBuilding()) { + return shooter; + } else { + return shooter; } } } + return null; +} + getDamage() { return this.getStats(this.level).damage } diff --git a/packages/junon-io/server/entities/player.js b/packages/junon-io/server/entities/player.js index 2e1cdd2c..7fbb5705 100644 --- a/packages/junon-io/server/entities/player.js +++ b/packages/junon-io/server/entities/player.js @@ -5164,9 +5164,27 @@ class Player extends BaseEntity { return true } - getDamage() { - let damage = super.getDamage() - return Math.floor(this.getDamageMultiplier() * damage) + getWeapon() { + const activeItem = this.getActiveItem(); + if (activeItem && activeItem.instance && typeof activeItem.instance.isWeapon === 'function' && activeItem.instance.isWeapon()) { + return activeItem.instance; + } + return null; +} + + getDamage(attackTarget) { + const weapon = this.getWeapon(); + let damage; + + if (weapon) { + damage = weapon.getDamage(attackTarget); + } else { + damage = super.getDamage(); + } + + const damageMultiplier = this.getDamageMultiplier(attackTarget); + + return Math.floor(damage * damageMultiplier); } diff --git a/packages/junon-io/server/entities/projectiles/base_projectile.js b/packages/junon-io/server/entities/projectiles/base_projectile.js index 543442c1..45a5aaad 100644 --- a/packages/junon-io/server/entities/projectiles/base_projectile.js +++ b/packages/junon-io/server/entities/projectiles/base_projectile.js @@ -24,7 +24,8 @@ class BaseProjectile extends BaseEntity { super(sector, { id: data.id, x: data.source.x, y: data.source.y, w: data.w, h: data.h }) this.weapon = data.weapon - this.sourceEntity = data.weapon + this.owner = data.owner ? data.owner : data.weapon.owner + this.sourceEntity = this.owner this.destinationEntity = data.destinationEntity if (data.keyValueMap) { @@ -32,7 +33,6 @@ class BaseProjectile extends BaseEntity { this.shouldHitFloor = data.keyValueMap.shouldHitFloor } - this.owner = data.owner ? data.owner : data.weapon.owner this.source = data.source this.destination = data.destination From e5c0b9eae9120ac03271f4a2c78e0cbe246bec39 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Tue, 5 Aug 2025 11:10:11 +0800 Subject: [PATCH 21/23] small change --- packages/junon-io/server/commands/spawn_mob.js | 4 ---- packages/junon-io/server/entities/mobs/base_mob.js | 11 ++--------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/junon-io/server/commands/spawn_mob.js b/packages/junon-io/server/commands/spawn_mob.js index 0ae53058..c5f9146e 100644 --- a/packages/junon-io/server/commands/spawn_mob.js +++ b/packages/junon-io/server/commands/spawn_mob.js @@ -141,10 +141,6 @@ class SpawnMob extends BaseCommand { } } - if (!keyValueMap["attackables"]) { - data.attackables = ["players", "mobs", "buildings"] - } - const mobs = this.sector.spawnMob(data) if (keyValueMap["raid"] === "true") { diff --git a/packages/junon-io/server/entities/mobs/base_mob.js b/packages/junon-io/server/entities/mobs/base_mob.js index a2fe972d..0f4e029e 100644 --- a/packages/junon-io/server/entities/mobs/base_mob.js +++ b/packages/junon-io/server/entities/mobs/base_mob.js @@ -2166,7 +2166,6 @@ getDamage(attackTarget) { totalDamage = super.getDamage(); } - // 處理自訂傷害覆蓋 (此處的自訂傷害是針對 Mob 本身) if (this.sector) { if (this.sector.entityCustomStats[this.id] && typeof this.sector.entityCustomStats[this.id].damage !== 'undefined') { totalDamage = this.sector.entityCustomStats[this.id].damage; @@ -2196,19 +2195,14 @@ getDamage(attackTarget) { getDamage(attackTarget) { const weapon = this.getWeapon(); let totalDamage; - - // 檢查 mob 是否裝備了武器 if (weapon) { - // 如果有武器,則傷害 = mob 基礎傷害 + 武器傷害 - const baseDamage = super.getDamage(); // 取得 mob 在 constants.json 中設定的空手傷害 - const weaponDamage = weapon.getDamage(attackTarget); // 取得武器自身的傷害 + const baseDamage = super.getDamage(); + const weaponDamage = weapon.getDamage(attackTarget); totalDamage = baseDamage + weaponDamage; } else { - // 如果沒有武器,則只使用 mob 的基礎傷害 totalDamage = super.getDamage(); } - // 檢查是否有來自伺服器設定的自訂傷害值,若有則覆蓋計算結果 (保留原有邏輯) if (this.sector) { if (this.sector.entityCustomStats[this.id] && typeof this.sector.entityCustomStats[this.id].damage !== 'undefined') { totalDamage = this.sector.entityCustomStats[this.id].damage; @@ -2217,7 +2211,6 @@ getDamage(attackTarget) { } } - // 最後,應用傷害乘數 (例如對建築物的加成) const damageMultiplier = this.getDamageMultiplier(attackTarget); return Math.floor(damageMultiplier * totalDamage); } From 8108ec7f08e2670d7540b172072e79e8e4296f21 Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Tue, 5 Aug 2025 13:17:20 +0800 Subject: [PATCH 22/23] (Maybe) fixed the bug that when players join sector and only see sky / allowed miamsa in peaceful mode --- packages/junon-io/server/entities/player.js | 13 +++++++++++++ packages/junon-io/server/entities/sector.js | 9 +++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/junon-io/server/entities/player.js b/packages/junon-io/server/entities/player.js index 7fbb5705..3497fef7 100644 --- a/packages/junon-io/server/entities/player.js +++ b/packages/junon-io/server/entities/player.js @@ -60,6 +60,7 @@ class Player extends BaseEntity { this.sessionId = socket.sessionId this.remoteAddress = Helper.getSocketRemoteAddress(socket) this.fingerprint = data.fingerprint + this.isClientReadyAndWaitingForSector = false if (this.sector.isZoomAllowed()) { this.screenWidth = this.socket.screenWidth @@ -2652,7 +2653,19 @@ class Player extends BaseEntity { onPlayerReady() { // we just got notified by client that they received connection + // if sector is not ready yet, wait for it to finish loading + if (!this.sector.sectorLoader.isFinished) { + this.isClientReadyAndWaitingForSector = true + // sectorLoader will call this.onPlayerReady again when it is done + return + } + + this.finishPlayerReadySetup(); + } + + finishPlayerReadySetup() { this.isPlayerReady = true + this.isClientReadyAndWaitingForSector = false for (let tutorialName in this.tutorialIndex) { this.sendClientTutorialIndex(tutorialName) diff --git a/packages/junon-io/server/entities/sector.js b/packages/junon-io/server/entities/sector.js index e253721c..5033a73d 100644 --- a/packages/junon-io/server/entities/sector.js +++ b/packages/junon-io/server/entities/sector.js @@ -871,6 +871,13 @@ class Sector { }) this.enableChunkInvalidations() + // after loading, we can rebuild invalidated chunks + this.forEachPlayer((player) => { + if (player.isClientReadyAndWaitingForSector) { + player.finishPlayerReadySetup(); + } + }) + this.game.postGameReady() } @@ -3131,8 +3138,6 @@ class Sector { } processMiasma() { - if (this.game.isPeaceful()) return - const isOneMinuteInterval = this.game.timestamp % (Constants.physicsTimeStep * 60) === 0 if (!isOneMinuteInterval) return From 24ce047884228a32a00275ee8d1319147763b10d Mon Sep 17 00:00:00 2001 From: notjc821 <9jc878@gmail.com> Date: Fri, 8 Aug 2025 09:32:11 +0800 Subject: [PATCH 23/23] $getEntityType --- packages/junon-io/client/main.ejs | 5 +++++ packages/junon-io/server/entities/event_handler.js | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/junon-io/client/main.ejs b/packages/junon-io/client/main.ejs index 5de7b128..81ca3d82 100644 --- a/packages/junon-io/client/main.ejs +++ b/packages/junon-io/client/main.ejs @@ -1014,6 +1014,11 @@ $entityId the type name of a building or entity. Ex. /chat @a The building type of entity number 2017 is $getBuildingType(2017) + + $getEntityType + $entityId + the type name of a building or entity. Ex. /chat @a The entity type of entity number 2017 is $getEntityType(2017) + $getDay None diff --git a/packages/junon-io/server/entities/event_handler.js b/packages/junon-io/server/entities/event_handler.js index 3161228b..3d468ee3 100644 --- a/packages/junon-io/server/entities/event_handler.js +++ b/packages/junon-io/server/entities/event_handler.js @@ -799,6 +799,18 @@ class EventHandler { } getBuildingType(entityId) { + let entity = this.game.getEntity(entityId) + + if (!entity) return "" + + if (typeof entity.getTypeName === 'function') { + return entity.getTypeName() + } + + return entity.type || "" + } + + getEntityType(entityId) { let player = this.game.getPlayerByName(entityId) if (player) { return "Player" @@ -1261,6 +1273,7 @@ class EventHandler { "$asin": true, "$acos": true, "$atan": true, + "$getEntityType" : true, } }