diff --git a/main.gd b/main.gd index da0a394..d11d185 100644 --- a/main.gd +++ b/main.gd @@ -206,7 +206,7 @@ var TOWER_DATA := [ {"name": "EMP-PULS", "cost": 210, "damage": 20.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118 · ALLE", "target_class": "both", "both_damage_scale": 1.0, "color": Color("77e8ff"), "unlock_cost": 24, "description": "Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden."}, {"name": "RAILGUN", "cost": 300, "damage": 77.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "PIERCING-STRAHL", "target_class": "both", "both_damage_scale": 0.70, "color": Color("c86bff"), "unlock_cost": 30, "description": "Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie."}, {"name": "FLAK", "cost": 210, "damage": 22.0, "range": 230.0, "rate": 0.42, "shot_speed": 920.0, "damage_type": "LUFT-EINZELZIEL", "target_class": "air", "color": Color("ff737c"), "unlock_cost": 0, "description": "Schnelle Abfangkanone gegen gegnerische Luftschiffe."}, - {"name": "ADMIN-TURM", "cost": 0, "damage": 1000000.0, "range": 3000.0, "rate": 0.08, "shot_speed": 3000.0, "damage_type": "TESTMODUS", "target_class": "both", "color": Color("fff2a1"), "unlock_cost": 0, "description": "Entwicklertest: besiegt jedes Ziel mit einem Schuss."} + {"name": "ADMIN-TURM", "cost": 0, "damage": 1000000.0, "range": 650.0, "rate": 0.08, "shot_speed": 3000.0, "damage_type": "TESTMODUS", "target_class": "both", "color": Color("fff2a1"), "unlock_cost": 0, "description": "Entwicklertest: besiegt jedes Ziel mit einem Schuss."} ] var ENEMY_DATA := [ @@ -638,6 +638,10 @@ func _apply_balance_config(config: Dictionary) -> bool: enemy["reward"] = clampi(int(values.get("reward", enemy.reward)), 0, 50000) enemy["breach_damage"] = clampi(int(values.get("breachDamage", enemy.breach_damage)), 1, 100) enemy["shield_resistance"] = clampf(float(values.get("shieldResistance", enemy.shield_resistance)), 0.01, 1.0) + var admin_config = config.get("admin_tower", {}) + if admin_config is Dictionary: + TOWER_DATA[ADMIN_TOWER_TYPE]["range"] = clampf(float(admin_config.get("range", 650.0)), 80.0, 1200.0) + TOWER_DATA[ADMIN_TOWER_TYPE]["rate"] = clampf(float(admin_config.get("cooldown", 0.08)), 0.04, 20.0) return true @@ -936,6 +940,8 @@ func _find_target(tower: Dictionary) -> Dictionary: var best_is_shielded := false var effective_range := _tower_range(tower.type, tower.level) for enemy in enemies: + if coop_mode and int(tower.type) == ADMIN_TOWER_TYPE and int(enemy.get("route_id", 0)) != (0 if tower.pos.x < COOP_MIRROR_X else 1): + continue if _tower_can_target(tower.type, int(enemy.type)) and tower.pos.distance_to(enemy.pos) <= effective_range: var route: PackedVector2Array = _enemy_route(enemy) var progress: float = enemy.path_index * 1000.0 - enemy.pos.distance_to(route[enemy.path_index]) @@ -1042,6 +1048,14 @@ func _update_projectiles(delta: float) -> void: func _apply_damage(enemy: Dictionary, amount: float, owner := 0, weapon_type := -1) -> void: if not enemies.has(enemy): return + if weapon_type == ADMIN_TOWER_TYPE: + enemy.shield = false + enemy.shield_hp = 0.0 + enemy.hp = 0.0 + _award_credits(owner, int(enemy.reward)) + _spawn_burst(enemy.pos, Color("ff6b3d"), 11 + enemy.type * 4) + enemies.erase(enemy) + return var applied_damage := amount if bool(enemy.get("shield", false)): if weapon_type == 1: diff --git a/packaging/windows/installer.nsi b/packaging/windows/installer.nsi index a159e83..9fd98e7 100644 --- a/packaging/windows/installer.nsi +++ b/packaging/windows/installer.nsi @@ -6,7 +6,7 @@ RequestExecutionLevel user !include "LogicLib.nsh" !define PRODUCT_NAME "Void Bastion" -!define PRODUCT_VERSION "0.1.0-alpha.14" +!define PRODUCT_VERSION "0.1.0-alpha.15" !define PRODUCT_PUBLISHER "GerGames" !define PRODUCT_WEBSITE "https://void-bastion.gergames.de/" !define PRODUCT_EXE "Void-Bastion.exe" @@ -21,7 +21,7 @@ BrandingText "GerGames" ShowInstDetails show ShowUninstDetails show -VIProductVersion "0.1.0.14" +VIProductVersion "0.1.0.15" VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}" VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}" VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup" diff --git a/project.godot b/project.godot index f311e48..cb63998 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Void Bastion" -config/version="0.1.0-alpha.14" +config/version="0.1.0-alpha.15" run/main_scene="res://main.tscn" config/features=PackedStringArray("4.7") config/icon="res://assets/app_icon.png" diff --git a/server/nakama/runtime/build/index.js b/server/nakama/runtime/build/index.js index 553e1f5..f8bf4d0 100644 --- a/server/nakama/runtime/build/index.js +++ b/server/nakama/runtime/build/index.js @@ -57,7 +57,7 @@ var DIFFICULTIES = [ ]; function defaultBalanceConfig() { - return {schema:2,towers:JSON.parse(JSON.stringify(TOWERS)),enemies:JSON.parse(JSON.stringify(ENEMIES))}; + return {schema:3,towers:JSON.parse(JSON.stringify(TOWERS)),enemies:JSON.parse(JSON.stringify(ENEMIES)),admin_tower:{range:650,cooldown:0.08}}; } function boundedNumber(value, fallback, minimum, maximum, integer) { var number = Number(value); @@ -93,7 +93,12 @@ function sanitizeBalanceConfig(input) { shieldResistance:boundedNumber(candidate.shieldResistance,base.shieldResistance,0.01,1,false) }; }); - return {schema:2,towers:towers,enemies:enemies}; + var adminInput=input.admin_tower||{}; + var adminTower={ + range:boundedNumber(adminInput.range,defaults.admin_tower.range,80,1200,false), + cooldown:boundedNumber(adminInput.cooldown,defaults.admin_tower.cooldown,0.04,20,false) + }; + return {schema:3,towers:towers,enemies:enemies,admin_tower:adminTower}; } function readBalanceConfig(nk) { var record = nk && typeof nk.storageRead === "function" ? readStorageRecord(nk,BALANCE_COLLECTION,BALANCE_KEY,SYSTEM_USER_ID) : null; @@ -108,6 +113,8 @@ function readBalanceConfig(nk) { var legacyBoss = storedConfig.enemies[5] || {}; if (Number(legacyTower.damage) === 20) config.towers[6].damage = 22; if (Number(legacyBoss.hp) === 740) config.enemies[5].hp = 620; + } + if (Number(storedConfig.schema || 1) < 3) { updatedAt = Date.now(); if (nk && typeof nk.storageWrite === "function") { try { nk.storageWrite([{collection:BALANCE_COLLECTION,key:BALANCE_KEY,userId:SYSTEM_USER_ID,value:{config:config,updated_at:updatedAt},permissionRead:0,permissionWrite:0,version:record.version||"*"}]); } @@ -574,7 +581,10 @@ function matchLeave(ctx,logger,nk,dispatcher,tick,state,presences) { } function parseMessage(nk,message) { try { return JSON.parse(nk.binaryToString(message.data)||"{}"); } catch (error) { return null; } } function towerInfo(state,type) { - if (type===ADMIN_TOWER_TYPE) return {cost:0,damage:1000000,range:3000,cooldown:0.08,targetClass:"both",bothScale:1}; + if (type===ADMIN_TOWER_TYPE) { + var admin=state.balance&&state.balance.admin_tower||defaultBalanceConfig().admin_tower; + return {cost:0,damage:1000000,range:admin.range,cooldown:admin.cooldown,targetClass:"both",bothScale:1}; + } return state.balance && state.balance.towers[type] || TOWERS[type]; } function enemyInfo(state,type) { return state.balance && state.balance.enemies[type] || ENEMIES[type]; } @@ -659,6 +669,7 @@ function spawnEnemy(state) { state.enemies.push({id:state.nextEnemyId++,type:type,x:route[0][0],y:route[0][1],path_index:1,route_id:routeId,hp:base.hp*hullScale,max_hp:base.hp*hullScale,speed:base.speed*(1+state.wave*0.012)*(boss?0.82:1)*d.speed,reward:Math.floor((base.reward+state.wave)*(boss?4:1)*d.income),angle:0,boss:boss,shield:hasShield,shield_hp:maxShieldHp,max_shield_hp:maxShieldHp,shield_resistance:base.shieldResistance}); } function applyDamageToEnemy(enemy,damage,towerType) { + if (towerType===ADMIN_TOWER_TYPE) { enemy.shield=false; enemy.shield_hp=0; enemy.hp=0; return; } var applied=damage; if (enemy.shield) { if (towerType===1) { @@ -688,6 +699,7 @@ function updateTowers(state,delta) { var t=state.towers[i],tower=towerInfo(state,t.type); t.cooldown-=delta; var range=towerRange(state,t),targetIndex=-1,best=-1,bestShield=false; for (var j=0;jbest)) { best=p; bestShield=shieldPriority; targetIndex=j; } diff --git a/server/nakama/tests/runtime_sim_test.js b/server/nakama/tests/runtime_sim_test.js index 2e83d5c..eb0cfa4 100644 --- a/server/nakama/tests/runtime_sim_test.js +++ b/server/nakama/tests/runtime_sim_test.js @@ -44,10 +44,13 @@ assert.strictEqual(adminBalance.config.towers[0].damage, 22); const editedBalance = JSON.parse(JSON.stringify(adminBalance.config)); editedBalance.towers[0].damage = 31; editedBalance.enemies[0].hp = 90; +editedBalance.admin_tower.range = 700; +editedBalance.admin_tower.cooldown = 0.12; assert.throws(() => context.rpcSetGameBalance({ userId: "pilot-b" }, {}, balanceNk, JSON.stringify({ config: editedBalance })), /server-to-server/); assert.strictEqual(JSON.parse(context.rpcSetGameBalance({}, {}, balanceNk, JSON.stringify({ config: editedBalance }))).accepted, true); assert.strictEqual(JSON.parse(context.rpcGetGameBalance({ userId: "pilot-b" }, {}, balanceNk, "{}")).config.towers[0].damage, 31); assert.strictEqual(context.matchInit({}, {}, balanceNk, { difficulty: 1 }).state.balance.enemies[0].hp, 90); +assert.strictEqual(context.matchInit({}, {}, balanceNk, { difficulty: 1 }).state.balance.admin_tower.range, 700); assert.throws(() => context.rpcSetGameBalance({}, {}, balanceNk, JSON.stringify({ config: { towers: [], enemies: [] } })), /invalid balance schema/); const legacyStorage = new Map(); const legacyKey = `game_balance:${context.SYSTEM_USER_ID}:global`; @@ -63,11 +66,12 @@ const migrationNk = { storageWrite(ops) { for (const op of ops) legacyStorage.set(`${op.collection}:${op.userId}:${op.key}`, { ...op, version: "8" }); } }; const migratedBalance = context.readBalanceConfig(migrationNk); -assert.strictEqual(migratedBalance.config.schema, 2); +assert.strictEqual(migratedBalance.config.schema, 3); assert.strictEqual(migratedBalance.config.towers[0].damage, 31, "Balance migrations must preserve unrelated admin tuning."); assert.strictEqual(migratedBalance.config.towers[6].damage, 22, "Unchanged legacy Flak defaults should migrate to the new value."); assert.strictEqual(migratedBalance.config.enemies[5].hp, 620, "Unchanged legacy boss HP should migrate to the new value."); -assert.strictEqual(legacyStorage.get(legacyKey).value.config.schema, 2, "Migrated balance should persist for future matches."); +assert.strictEqual(migratedBalance.config.admin_tower.range, 650, "Older balance settings should receive the safer admin range."); +assert.strictEqual(legacyStorage.get(legacyKey).value.config.schema, 3, "Migrated balance should persist for future matches."); const hostId = "11111111-1111-1111-1111-111111111111"; const friendId = "22222222-2222-2222-2222-222222222222"; const sentInvites = []; @@ -401,11 +405,14 @@ assert.ok(state.towers.some(tower => tower.type === 7 && tower.owner === 0), "Au const adminCombatState = { balance: state.balance, towers: [{ id: 99, type: 7, x: 2200, y: 600, owner: 0, level: 1, cooldown: 0, angle: 0 }], - enemies: [{ id: 99, type: 8, x: 2250, y: 600, hp: 100000, max_hp: 100000, shield: true, shield_hp: 1000, shield_resistance: 0.28 }], + enemies: [ + { id: 99, type: 8, x: 2250, y: 600, route_id: 1, path_index: 1, hp: 100000, max_hp: 100000, shield: true, shield_hp: 1000, shield_resistance: 0.28 }, + { id: 100, type: 8, x: 2250, y: 610, route_id: 0, path_index: 1, hp: 100000, max_hp: 100000, shield: true, shield_hp: 1000, shield_resistance: 0.28 } + ], shots: [], credits: [0, 0] }; context.updateTowers(adminCombatState, 0.1); -assert.strictEqual(adminCombatState.enemies.length, 0, "One admin turret shot should destroy even a shielded enemy."); +assert.deepStrictEqual(adminCombatState.enemies.map(enemy => enemy.id), [100], "The admin turret must one-shot shielded enemies only on its own route."); state = context.matchLoop({}, logger, nk, dispatcher, 57, state, [message(presenceA, 13, {})]).state; assert.strictEqual(state.wave, 1);