diff --git a/build.gradle b/build.gradle index 88718fdea4..f4ff0f9303 100644 --- a/build.gradle +++ b/build.gradle @@ -217,8 +217,9 @@ project(":tests"){ dependencies{ testImplementation project(":core") - testImplementation('org.junit.jupiter:junit-jupiter-api:5.1.0') - testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.1.0') + testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.1" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" compile arcModule("backends:backend-headless") } @@ -250,7 +251,7 @@ project(":net"){ dependencies{ compile project(":core") compile "org.lz4:lz4-java:1.4.1" - compile 'com.github.Anuken:kryonet:38ca8d51b5763ebe463ed973a63b64390ff51416' + compile 'com.github.Anuken:kryonet:a64d2280880e80566ca1bdaffa55de43e51cad38' compile 'com.github.Anuken:WaifUPnP:05eb46bc577fd7674596946ba288c96c0cedd893' } } diff --git a/core/assets/bundles/bundle_uk_UA.properties b/core/assets/bundles/bundle_uk_UA.properties index 6bc5700d53..c78427623f 100644 --- a/core/assets/bundles/bundle_uk_UA.properties +++ b/core/assets/bundles/bundle_uk_UA.properties @@ -1,6 +1,6 @@ text.credits.text = Створив [ROYAL]Anuken[] - [SKY]anukendev@gmail.com[]\n\nЄ питання по грі або проблеми с перекладом? Іди в офіційний сервер discord Mindustry в канал #український. text.credits = Автори -text.contributors = Перекладачі та Контриб'ютори +text.contributors = Перекладачі та Помічники text.discord = Приєднуйтесь до нашого Discord! text.link.discord.description = Офіційний discord-сервер Mindustry text.link.github.description = Код гри @@ -10,6 +10,7 @@ text.link.itch.io.description = Itch.io сторінка з веб-версіє text.link.google-play.description = Скачати з Google Play для Android text.link.wiki.description = Офіційна Mindustry вікі (англ.) text.linkfail = Не вдалося відкрити посилання!\nURL-адреса скопійовано у ваш буфер обміну. +text.screenshot = Скріншот збережено в {0} text.gameover = Гру закінчено text.gameover.pvp = [accent] {0}[] команда перемогла! text.sector.gameover = Цей сектор було втрачено. Повторно висадитися? @@ -23,15 +24,15 @@ text.level.select = Вибір мапи text.level.mode = Режим гри: text.construction.desktop = Щоб скасувати вибір блоку або припинити будівництво, [accent] скористайтеся пробілом[]. text.construction.title = Інструкція з будівництва блоків -text.construction = Ви тільки що перейшли в режим будівництва[accent] блоків[].\n\nЩоб розпочати розміщення, просто торкніться підходящого місця поруч із вашим кораблем.\nПісля вибору деяких блоків натисніть прапорець, щоб підтвердити, і ваш корабель почне будувати їх.\n\n- [accent]Вилучіть блоки[] з вашого вибору, торкнувшись їх.\n- [accent]Перемістіть виділення[] утримуючи та перетягнувши будь-який блок у виділенні.\n- [accent]Розташуйте блоки у лінію[], торкнувшись і утримуючи порожнє місце, а потім перетягуючи в потрібному напрямку.\n- [accent]Скасуйте розміщення блоків[] натиснувши X внизу ліворуч. +text.construction = Ви тільки що перейшли в режим будівництва[accent] блоків[].\n\nЩоб розпочати розміщення, просто торкніться підходящого місця поруч із вашим кораблем.\nПісля вибору деяких блоків натисніть прапорець, щоб підтвердити, і ваш корабель почне будувати їх.\n\n- [accent]Вилучіть блоки[] з вашого вибору, торкнувшись їх.\n- [accent]Перемістіть виділення[] утримуючи та перетягнувши будь-який блок у виділенні.\n- [accent]Розташуйте блоки у лінію[], торкнувшись і утримуючи порожнє місце, а потім перетягуючи в потрібному напрямку.\n- [accent]Скасуйте розміщення блоків[] натиснувши X внизу праворуч. text.deconstruction.title = Інструкція з деконструкції блоків -text.deconstruction = Ви тільки що перешли в [accent] режим деконструкції блоків[].\n\nЩоб почати руйнувати, просто торкніться блоку поруч із вашим кораблем.\nПісля вибору деяких блоків натисніть прапорець, щоб підтвердити, і ваш корабель почне їх деконструювати.\n\n- [accent]Вилучіть блоки[] з вашого вибору, торкнувшись їх.\n- [accent]Вилучіть блоки в зоні[] , торкнувшись і утримуючи порожнє місце, потім перетягніть у потрібному напрямку.\n- [accent]Скасуйте деконструкцію або виділення[] натиснувши X внизу ліворуч. +text.deconstruction = Ви тільки що перешли в [accent] режим деконструкції блоків[].\n\nЩоб почати руйнувати, просто торкніться блоку поруч із вашим кораблем.\nПісля вибору деяких блоків натисніть прапорець, щоб підтвердити, і ваш корабель почне їх деконструювати.\n\n- [accent]Вилучіть блоки[] з вашого вибору, торкнувшись їх.\n- [accent]Вилучіть блоки в зоні[] , торкнувшись і утримуючи порожнє місце, потім перетягніть у потрібному напрямку.\n- [accent]Скасуйте деконструкцію або виділення[] натиснувши X внизу праворуч. text.showagain = Не показувати знову до наступного сеансу text.coreattack = < Ядро під атакою! > text.unlocks = Розблоковане text.savegame = Зберегти гру text.loadgame = Завантажити гру -text.joingame = Приєднатися +text.joingame = Мережева гра text.addplayers = Дод/Видалити гравців text.customgame = Користувальницька гра text.sectors = Сектори @@ -39,7 +40,7 @@ text.sector = Обраний сектор: [LIGHT_GRAY]{0} text.sector.time = Час: [LIGHT_GRAY]{0} text.sector.deploy = Висадитися text.sector.abandon = Відступити -text.sector.abandon.confirm = Ви впевнені, що хочете відступити?\nЦе не може бути скасовано! +text.sector.abandon.confirm = Ви впевнені, що хочете відступити?\nПрогрес в секторі не можна відновити! text.sector.resume = Продовжити text.sector.locked = [scarlet][[Незавершений] text.sector.unexplored = [accent][[Недосліджений] @@ -72,6 +73,7 @@ text.nextmission = Наступна місія text.maps.none = [LIGHT_GRAY]Карт не знайдено! text.about.button = Про гру text.name = Нік: +text.noname = Спочатку придумайте[accent] собі нікнейм[]. text.filename = Ім'я файлу: text.unlocked = Новий блок розблоковано! text.unlocked.plural = Нові блоки розблоковано! @@ -84,12 +86,14 @@ text.server.kicked.sectorComplete = Сектор завойовано. text.server.kicked.sectorComplete.text = Ваша місія завершена. \nСервер продовжить роботу і висадить Вас в наступному секторі. text.server.kicked.clientOutdated = Застарілий клієнт! Оновіть свою гру! text.server.kicked.serverOutdated = Застарілий сервер! Попросіть адміністратора серверу оновити сервер/гру! +text.server.kicked.commitMismatch = Номер збірки серверу не співпадає з номером вашої збірки клієнта. Скачайте підходящу версію. text.server.kicked.banned = Ви були заблоковані на цьому сервері. text.server.kicked.recentKick = Нещодавно Вас вигнали(кікнули). \nПочекайте трохи перед наступним підключенням. text.server.kicked.nameInUse = На цьому сервері є хтось \nз таким ніком. text.server.kicked.nameEmpty = Ваш нікнейм має містити принаймні один символ або цифру. text.server.kicked.idInUse = Ви вже на цьому сервері! Підключення двох облікових записів не допускається. text.server.kicked.customClient = Цей сервер не підтримує користувальницькі збірки. Завантажте офіційну версію. +text.server.kicked.gameover = Гра завершена! text.host.info = Кнопка [accent]Сервер[] розміщує сервер на порті [scarlet]6567[]. \nКористувачі, які знаходяться у тій же [LIGHT_GRAY] WiFi або локальній мережі[] повинні бачити ваш сервер у своєму списку серверів.\n\nЯкщо ви хочете, щоб люди могли приєднуватися з будь-якої точки через IP, то [accent] переадресація порту [] обов'язкова.\n\n[LIGHT_GRAY] Примітка. Якщо у вас виникли проблеми з підключенням до вашої локальної гри, переконайтеся, що ви дозволили Mindustry доступ до вашої локальної мережі в налаштуваннях брандмауера. text.join.info = Тут ви можете ввести [accent]IP серверу[] для підключення або знайти сервери у [accent]локальній мережі[] для підключення до них.\nПідтримується локальна мережа(LAN) і широкосмугова мережа(WAN).\n\n[LIGHT_GRAY] Примітка. Тут немає автоматичного глобального списку серверів; якщо ви хочете підключитися до когось через IP, вам доведеться попросити створювача серверу дати свій ip. text.hostserver = Запустити сервер @@ -182,7 +186,7 @@ text.quit.confirm = Ви впевнені що хочете вийти? text.changelog.title = Журнал змін text.changelog.loading = Отримання журналу змін... text.changelog.error.android = [accent]Зверніть увагу, що іноді журнал змін не працює на ОС Android 4.4 або на нижчій версії!\nЦе пов'язано з внутрішньою помилкою Android. -text.changelog.error.ios = [accent]В настоящее время журнал изменений не поддерживается iOS. +text.changelog.error.ios = [accent]Журнал змін наразі не підтримується iOS. text.changelog.error = [scarlet]Помилка отримання журналу змін!\nПеревірте підключення до Інтернету. text.changelog.current = [yellow][[Поточна версія] text.changelog.latest = [accent][[Остання версія] @@ -264,8 +268,10 @@ text.connectfail = [crimson]Не вдалося підключитися до с text.error.unreachable = Сервер не доступний. text.error.invalidaddress = Некоректна адреса. text.error.timedout = Час очікувування вийшов.\nПереконайтеся, що адреса коректна і що власник сервера налаштував переадресацію порту! -text.error.mismatch = Ошибка пакету:\nможливе невідповідність версії клієнта / сервера.\nПереконайтеся, що у Вас та у володара сервера встановлена остання версія Mindustry! +text.error.mismatch = Помилка пакету:\nможливе невідповідність версії клієнта / сервера.\nПереконайтеся, що у Вас та у володара сервера встановлена остання версія Mindustry! text.error.alreadyconnected = Ви вже підключилися. +text.error.mapnotfound = Файл мапи не знайдено +text.error.io = Мережева помилка введення-виведення text.error.any = Невідома мережева помилка text.settings.language = Мова text.settings.reset = Скинути за замовчуванням @@ -290,7 +296,7 @@ text.blocks.unknown = [LIGHT_GRAY]??? text.blocks.blockinfo = Інформація про блок text.blocks.powercapacity = Місткість енергії text.blocks.powershot = Енергія/постріл -text.blocks.targetsair = Атакуе повітряних ворогів? +text.blocks.targetsair = Атакуе повітряних ворогів text.blocks.itemspeed = Швидкість переміщення ресурсів text.blocks.shootrange = Діапазон дії text.blocks.size = Розмір @@ -346,6 +352,7 @@ text.category.items = Предмети text.category.crafting = Створення text.category.shooting = Стрільба text.category.optional = Додаткові поліпшення +setting.indicators.name = Показувати у сторону союзників setting.autotarget.name = Авто-ціль setting.fpscap.name = Макс. FPS setting.fpscap.none = Необмежений @@ -374,12 +381,15 @@ setting.crashreport.name = Надіслати анонімні звіти про text.keybind.title = Налаштування управління category.general.name = Основне category.view.name = Перегляд -category.multiplayer.name = Мультиплеєр +category.multiplayer.name = Мережева гра command.attack = Атакувати command.retreat = Відступити command.patrol = Патрулювати +keybind.gridMode.name = Вибрати блок +keybind.gridModeShift.name = Вибрати категорію keybind.press = Натисніть клавішу... keybind.press.axis = Натисніть клавішу... +keybind.screenshot.name = Скріншот мапи keybind.move_x.name = Рух по осі x keybind.move_y.name = Рух по осі x keybind.select.name = ВибратиПостріл @@ -410,6 +420,8 @@ mode.freebuild.name = Вільне\nбудівництво mode.freebuild.description = В режимі "Пісочниця" треба самим добувати ресурси та хвилі йдуть за вашим бажанням. mode.pvp.name = PVP mode.pvp.description = боріться проти інших гравців. +mode.attack.name = Атака +mode.attack.descrption = Немає хвиль, мета - знищити базу противника. content.item.name = Предмети content.liquid.name = Рідини content.unit.name = Бойові одиниці @@ -424,18 +436,18 @@ item.lead.description = Базовий стартовий матеріал. Ши item.coal.name = Вугілля item.coal.description = Загальне та легкодоступне паливо. item.dense-alloy.name = Щільний сплав -item.dense-alloy.description = Жорсткий сплав вироблений зі свинця та міді. Використовується в передових транспортних блоках та високорівневих свердлах. +item.dense-alloy.description = Сплав, котрий вироблений зі свинця та міді. Використовується в передових транспортних блоках та високорівневих свердлах. item.titanium.name = Титан item.titanium.description = Рідкий суперлегкий метал широко використовується в рідкому транспорті, свердлах та літальних апаратах. item.thorium.name = Торій item.thorium.description = Густий, радіоактивний метал, що використовується як структурна підтримка та ядерне паливо. -item.silicon.name = Кремень +item.silicon.name = Кремній item.silicon.description = Надзвичайно корисний напівпровідник з застосуванням в сонячних батареях та складній електроніці. item.plastanium.name = Пластиній item.plastanium.description = Легкий, пластичний матеріал, що використовується в сучасних літальних апаратах та у боєприпасах для фрагментації. item.phase-fabric.name = Фазова тканина -item.phase-fabric.description = Невагоме речовина, що використовується в сучасній електроніці і технології самовідновлення. -item.surge-alloy.name = Високоміцний сплав +item.phase-fabric.description = Невагоме речовина, що використовується в сучасній електроніці і технології самовідновлення. Не для вишивання. +item.surge-alloy.name = Кінетичний сплав item.surge-alloy.description = Передовий сплав з унікальними електричними властивостями. item.biomatter.name = Біоматерія item.biomatter.description = Скупчення органічної муси; використовується для перетворення в нафту або як паливо. @@ -450,7 +462,7 @@ liquid.lava.name = Лава liquid.oil.name = Нафта liquid.cryofluid.name = Кріогенна рідина mech.alpha-mech.name = Альфа -mech.alpha-mech.weapon = Звичайний кулемет +mech.alpha-mech.weapon = Тяжкий кулемет mech.alpha-mech.ability = Виклик дронів mech.alpha-mech.description = Стандартний мех для настільних пристроїв. Має пристойну швидкість і урон; може створити до 3-х дронів для збільшення можливості перемоги. mech.delta-mech.name = Дельта @@ -460,10 +472,10 @@ mech.delta-mech.description = Швидкий, легкоброньований mech.tau-mech.name = Тау mech.tau-mech.weapon = Відновлювальний лазер mech.tau-mech.ability = Відновлювальний спалах -mech.tau-mech.description = Мех підтримки. Зцілює союзницькі блоки, стріляючи в них. Може зцілити союзників у радіусі зі своєю здатністю для ремонту. +mech.tau-mech.description = Мех підтримки. Лагодить союзницькі блоки, стріляючи в них. Може зцілити союзників у радіусі зі своєю здатністю для ремонту. mech.omega-mech.name = Омега -mech.omega-mech.weapon = Купа ракет -mech.omega-mech.ability = Захисна конфігурація +mech.omega-mech.weapon = Ракометний пулемет +mech.omega-mech.ability = Поглинання урона mech.omega-mech.description = Громіздкий і добре броньований мех, зроблений для фронтових нападів. Його здатність може блокувати до 90% вхідного урона. mech.dart-ship.name = Дротик mech.dart-ship.weapon = Ретранслятор @@ -474,7 +486,7 @@ mech.javelin-ship.weapon = Вибухові ракети mech.javelin-ship.ability = Генератор дуг mech.trident-ship.name = Тризубець mech.trident-ship.description = Важкий бомбардувальник. Досить добре броньований. -mech.trident-ship.weapon = Вантажний відсік з бомбами +mech.trident-ship.weapon = Бомби mech.glaive-ship.name = Спис mech.glaive-ship.description = Великий, добре броньований бойовий корабель. Оснащений запальним ретранслятором. Гарне прискорення і максимальна швидкість. mech.glaive-ship.weapon = Вогняний кулемет @@ -493,11 +505,11 @@ text.mech.ability = [LIGHT_GRAY]Здібність: {0} text.liquid.heatcapacity = [LIGHT_GRAY]Теплоємність: {0} text.liquid.viscosity = [LIGHT_GRAY]В'язкість: {0} text.liquid.temperature = [LIGHT_GRAY]Температура: {0} -block.constructing = {0}[LIGHT_GRAY](В процесі) +block.constructing = {0}\n[LIGHT_GRAY](В процесі) block.spawn.name = Місце появи ворога block.core.name = Ядро block.space.name = Пустота -block.metalfloor.name = Металічна підлога +block.metalfloor.name = Металева плитка block.deepwater.name = Глибоководдя block.water.name = Вода block.lava.name = Лава @@ -591,10 +603,10 @@ block.oil-extractor.name = Екстрактор нафти block.spirit-factory.name = Завод дронов "Призрак" block.phantom-factory.name = Завод дронов "Фантом" block.wraith-factory.name = Завод винищувачів "Примара" -block.ghoul-factory.name = Завод бомбардувальників "Ґуль" -block.dagger-factory.name = Завод мехов "Разведчик" -block.titan-factory.name = Завод мехов "Титан" -block.fortress-factory.name = Завод мехов "Крепость" +block.ghoul-factory.name = Завод бомбардувальників-винищувачів "Ґуль" +block.dagger-factory.name = Завод мехів "Кинджал" +block.titan-factory.name = Завод мехів "Титан" +block.fortress-factory.name = Завод мехів "Крепость" block.revenant-factory.name = Завод бомбардировщиков "Потусторонний убийца" block.repair-point.name = Ремонтний пункт block.pulse-conduit.name = Імпульсний водовід @@ -635,7 +647,7 @@ unit.spirit.name = Дрон-привид unit.spirit.description = Початковий дрон. З'являється в ядрі за замовчуванням. Автоматично добуває руди та ремонтує блоки. unit.phantom.name = Фантом unit.phantom.description = Покращений дрон. Автоматично добуває руди та ремонтує блоки. -unit.dagger.name = Розвідник +unit.dagger.name = Кинджал unit.dagger.description = Базова наземна бойова одиниця. Корисен у купі. unit.titan.name = Титан unit.titan.description = Улучшенная бронированная наземная боевая единица. Атакует наземные и воздушные цели. @@ -646,7 +658,7 @@ unit.wraith.description = Швидка бойова одиниця, котрий unit.fortress.name = Фортеця unit.fortress.description = Тяжка артилерійна наземна бойова одиниця. unit.revenant.name = Потойбічний вбивця -unit.revenant.description = Бойова одиниця з важкою лазерною зброєю. +unit.revenant.description = Важка бойова одиниця з лазерною зброєю. tutorial.begin = Ваша місія тут полягає в ліквідації[LIGHT_GRAY] противника[].\n\nПочнімо з[accent] видобутку міді[]. Щоб зробити це, торкніться мідної рудної жили біля вашого ядра. tutorial.drill = Ручна робота не ефективна\n[accent]Бури []можуть копати автоматично.\nПоставте один на мідній жилі. tutorial.conveyor = [accent]Конвейери[] використовуються для транспортування предметів в ядра.\nЗробіть лінію конвейерів від бурів до ядра. @@ -662,9 +674,9 @@ tutorial.silicondrill = Кремній потребує[accent] вугілляl[ tutorial.generator = Ця технологія потребує енергії.\nЗробіть[accent] генератор внутрішнього згорання[] для цього. tutorial.generatordrill = Генератор потребує вугілля.\nПобудуйте бур на вугільній жилі. tutorial.node = Енергії потребує транспортування\nСоздайте[accent] силовий вузол[] поруч з вашим генератором згорання, щоб передавати його енергію. -tutorial.nodelink = Енергія може бути передана через контактні енергетичні блоки та генератори, або з'єднані силові вузли.\n\nЗ'єднайте живлення, торкнувшись вузла та вибравши генератор і кремнієвий завод. +tutorial.nodelink = Енергія може бути передана через контактні енергетичні блоки та генератори, або з'єднані силові вузли.\n\nЗ'єднайте їх, торкнувшись вузла та вибравши генератор і кремнієвий завод. tutorial.silicon = Кремній почався створюватися. Отримайте трохи.\n\nРекомендується вдосконалити виробничу систему. -tutorial.daggerfactory = Побудуйте[accent] завод "Розвідник".[]\n\nЦе буде використано для створення мехів атаки. +tutorial.daggerfactory = Побудуйте[accent] завод "Кинджал".[]\n\nЦе буде використано для створення мехів атаки. tutorial.router = Фабрики потребують ресурсів для функціонування.\nСтворіть маршрутизатор для розподілення ресурсів з конвейера. tutorial.dagger = Зв'яжіть силовий вузол з заводом.\nЯк тільки вимоги будуть виконані, буде створено мех.\n\nЯкщо необхідно, то створіть ще бурів, генераторів та конвейерів tutorial.battle = [LIGHT_GRAY] Супротивник[] показав своє ядро.\nЗнищьте його з вашим мехом та бойовою одиницею. @@ -703,11 +715,11 @@ block.phase-conveyor.description = Поки гра знаходиться в 2D, block.junction.description = Назва говорить сама за себе. За допомогою нього можна зробити дві конвеєрні стрічки, які проходять через один одного і не змішуються. block.mass-driver.description = При наявності енергії передають ресурси на відстань 100 блоків, стріляючи в один-одного. block.smelter.description = Виробляє щільний сплав з міді і свинцю. Можна підвести пісок для прискорення виробництва. -block.arc-smelter.description = Покращена версія плавильного заводу. Вимагає енергію. Виробляє щільний сплав зміді і свинця.\nМожно підвести пісок для прискорення виробництва. +block.arc-smelter.description = Покращена версія плавильного заводу. Вимагає енергію. Виробляє щільний сплав з міді і свинця.\nМожно підвести пісок для прискорення виробництва. block.silicon-smelter.description = За допомогою піску, вугілля і енергії виробляє кремній. block.plastanium-compressor.description = Створює пластинійи з титану і нафти. Вимагає енергії. Для прискорення виробництва можна додати в компресор пісок. block.phase-weaver.description = Виробляє фазову тканину торію і піску. Вимагає багато енергії. -block.alloy-smelter.description = Створює високоміцний сплав з титану, кременя, міді і свинця. Вимагає енергію. +block.alloy-smelter.description = Створює кінетичний сплав з титану, кременя, міді і свинця. Вимагає енергію. block.pulverizer.description = Подрібнює камінь в пісок. Вимагає енергії. block.pyratite-mixer.description = Створює піротит з вугілля, свинцю і піску. Вимагає енергії. block.blast-mixer.description = Створює вибухонебезпечне з'єднання з нафти і піротіта. Для прискорення виробництва можна додати в мішалку пісок. @@ -770,12 +782,12 @@ block.router.description = Приймає елементи з одного на block.distributor.description = Розширений маршрутизатор, який рівномірно розбиває елементи на 7 різних напрямків. block.bridge-conveyor.description = Покращений блок транспортування предметів. Дозволяє транспортувати предмети понад 3 блоки над будь-якої місцевостю або будівлеє. block.alpha-mech-pad.description = Коли ви отримаєте достатньо енергії, перебудовує ваш корабель у [accent] Альфа[] мех. -block.itemsource.description = Безліченно виводить предмети. Лише пісочниця. -block.liquidsource.description = Безліченно виводить рідини. Лише пісочниця. -block.itemvoid.description = Знищує будь-які предмети, які входять, без використання енергії. Працює тільки в пісочниці. -block.powerinfinite.description = Нескінченність не межа. Безмежно виводить енергію. Лише пісочниця. -block.powervoid.description = Енергія просто йде в порожнечу. Лише пісочниця. -liquid.water.description = Зазвичай використовується для охолодження машин та переробки відходів. -liquid.lava.description = Можна перетворити в[LIGHT_GRAY] камінь[], який використовується для генерації енергії або використовуати як боєприпаси для певних турелей. -liquid.oil.description = Можна спалити, взірвати або використовувати як теплоносій. -liquid.cryofluid.description = Найефективніша рідина для охолодження. Рідина з температурою нижче ніж -273 градусів за Цельсієм. Може бути використана для прискорення стрільби турелей або для охолодження чогось. +block.itemsource.description = Безліченно виводить предмети. +block.liquidsource.description = Безліченно виводить рідини. +block.itemvoid.description = Знищує будь-які предмети, які входять, без використання енергії. +block.powerinfinite.description = Нескінченність не межа. Безмежно виводить енергію. +block.powervoid.description = Енергія просто йде в порожнечу. +liquid.water.description = Цю рідину можно підвести до бурів для прискорення швидкості видобутку або к турелям для прискорення стрілянини. +liquid.lava.description = Можна перетворити в[LIGHT_GRAY] камінь[]. +liquid.oil.description = Можна спалити, взірвати або використовувати для охолодження. +liquid.cryofluid.description = Рідина з температурою нижче ніж -273 градусів за Цельсієм. Може бути використана для прискорення стрільби турелей або для охолодження чогось. diff --git a/core/src/io/anuke/mindustry/content/blocks/CraftingBlocks.java b/core/src/io/anuke/mindustry/content/blocks/CraftingBlocks.java index 6098809ee5..feec001313 100644 --- a/core/src/io/anuke/mindustry/content/blocks/CraftingBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/CraftingBlocks.java @@ -22,7 +22,6 @@ public class CraftingBlocks extends BlockList implements ContentList{ craftEffect = BlockFx.smeltsmoke; result = Items.silicon; craftTime = 40f; - powerCapacity = 20f; size = 2; hasLiquids = false; flameColor = Color.valueOf("ffef99"); @@ -36,7 +35,6 @@ public class CraftingBlocks extends BlockList implements ContentList{ liquidCapacity = 60f; craftTime = 60f; output = Items.plastanium; - powerCapacity = 40f; size = 2; health = 320; hasPower = hasLiquids = true; @@ -52,7 +50,6 @@ public class CraftingBlocks extends BlockList implements ContentList{ craftEffect = BlockFx.smeltsmoke; result = Items.phasefabric; craftTime = 120f; - powerCapacity = 50f; size = 2; consumes.items(new ItemStack(Items.thorium, 4), new ItemStack(Items.sand, 10)); @@ -63,7 +60,6 @@ public class CraftingBlocks extends BlockList implements ContentList{ craftEffect = BlockFx.smeltsmoke; result = Items.surgealloy; craftTime = 75f; - powerCapacity = 60f; size = 3; useFlux = true; diff --git a/core/src/io/anuke/mindustry/content/blocks/DebugBlocks.java b/core/src/io/anuke/mindustry/content/blocks/DebugBlocks.java index 2554114642..b3906fb184 100644 --- a/core/src/io/anuke/mindustry/content/blocks/DebugBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/DebugBlocks.java @@ -39,28 +39,26 @@ public class DebugBlocks extends BlockList implements ContentList{ public void load(){ powerVoid = new PowerBlock("powervoid"){ { - powerCapacity = Float.MAX_VALUE; + consumes.power(Float.MAX_VALUE); } @Override public void init(){ super.init(); - stats.remove(BlockStat.powerCapacity); + stats.remove(BlockStat.powerUse); } }; powerInfinite = new PowerNode("powerinfinite"){ { - powerCapacity = 10000f; maxNodes = 100; outputsPower = true; consumesPower = false; } @Override - public void update(Tile tile){ - super.update(tile); - tile.entity.power.amount = powerCapacity; + public float getPowerProduction(Tile tile){ + return 10000f; } }; diff --git a/core/src/io/anuke/mindustry/content/blocks/DefenseBlocks.java b/core/src/io/anuke/mindustry/content/blocks/DefenseBlocks.java index 6a8b586c7b..9f67d7fe7f 100644 --- a/core/src/io/anuke/mindustry/content/blocks/DefenseBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/DefenseBlocks.java @@ -71,19 +71,18 @@ public class DefenseBlocks extends BlockList implements ContentList{ }}; mendProjector = new MendProjector("mend-projector"){{ - consumes.power(0.2f); + consumes.power(0.2f, 1.0f); size = 2; consumes.item(Items.phasefabric).optional(true); }}; overdriveProjector = new OverdriveProjector("overdrive-projector"){{ - consumes.power(0.35f); + consumes.power(0.35f, 1.0f); size = 2; consumes.item(Items.phasefabric).optional(true); }}; forceProjector = new ForceProjector("force-projector"){{ - consumes.power(0.2f); size = 3; consumes.item(Items.phasefabric).optional(true); }}; diff --git a/core/src/io/anuke/mindustry/content/blocks/DistributionBlocks.java b/core/src/io/anuke/mindustry/content/blocks/DistributionBlocks.java index df4ab9ea20..0266642557 100644 --- a/core/src/io/anuke/mindustry/content/blocks/DistributionBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/DistributionBlocks.java @@ -35,7 +35,7 @@ public class DistributionBlocks extends BlockList implements ContentList{ phaseConveyor = new ItemBridge("phase-conveyor"){{ range = 12; hasPower = true; - consumes.power(0.03f); + consumes.power(0.03f, 1.0f); }}; sorter = new Sorter("sorter"); diff --git a/core/src/io/anuke/mindustry/content/blocks/LiquidBlocks.java b/core/src/io/anuke/mindustry/content/blocks/LiquidBlocks.java index 78d41bb413..164f72fa44 100644 --- a/core/src/io/anuke/mindustry/content/blocks/LiquidBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/LiquidBlocks.java @@ -20,7 +20,6 @@ public class LiquidBlocks extends BlockList implements ContentList{ pumpAmount = 0.2f; consumes.power(0.015f); liquidCapacity = 30f; - powerCapacity = 20f; hasPower = true; size = 2; tier = 1; @@ -31,7 +30,6 @@ public class LiquidBlocks extends BlockList implements ContentList{ consumes.power(0.03f); liquidCapacity = 40f; hasPower = true; - powerCapacity = 20f; size = 2; tier = 2; }}; @@ -66,7 +64,7 @@ public class LiquidBlocks extends BlockList implements ContentList{ phaseConduit = new LiquidBridge("phase-conduit"){{ range = 12; hasPower = true; - consumes.power(0.03f); + consumes.power(0.03f, 1.0f); }}; } } diff --git a/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java b/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java index f9b866cbc0..843541ca64 100644 --- a/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/PowerBlocks.java @@ -4,6 +4,7 @@ import io.anuke.mindustry.content.Liquids; import io.anuke.mindustry.content.fx.BlockFx; import io.anuke.mindustry.game.ContentList; import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Tile; import io.anuke.mindustry.world.blocks.power.*; public class PowerBlocks extends BlockList implements ContentList{ @@ -13,48 +14,43 @@ public class PowerBlocks extends BlockList implements ContentList{ @Override public void load(){ combustionGenerator = new BurnerGenerator("combustion-generator"){{ - powerOutput = 0.09f; - powerCapacity = 40f; + powerProduction = 0.09f; itemDuration = 40f; }}; thermalGenerator = new LiquidHeatGenerator("thermal-generator"){{ maxLiquidGenerate = 2f; - powerCapacity = 40f; - powerPerLiquid = 0.35f; + powerProduction = 2f; generateEffect = BlockFx.redgeneratespark; size = 2; }}; turbineGenerator = new TurbineGenerator("turbine-generator"){{ - powerOutput = 0.28f; - powerCapacity = 40f; + powerProduction = 0.28f; itemDuration = 30f; - powerPerLiquid = 0.7f; consumes.liquid(Liquids.water, 0.05f); size = 2; }}; rtgGenerator = new DecayGenerator("rtg-generator"){{ - powerCapacity = 40f; size = 2; - powerOutput = 0.3f; + powerProduction = 0.3f; itemDuration = 220f; }}; solarPanel = new SolarGenerator("solar-panel"){{ - generation = 0.0045f; + powerProduction = 0.0045f; }}; largeSolarPanel = new SolarGenerator("solar-panel-large"){{ size = 3; - generation = 0.055f; + powerProduction = 0.055f; }}; thoriumReactor = new NuclearReactor("thorium-reactor"){{ size = 3; health = 700; - powerMultiplier = 1.1f; + powerProduction = 1.1f; }}; fusionReactor = new FusionReactor("fusion-reactor"){{ @@ -63,12 +59,12 @@ public class PowerBlocks extends BlockList implements ContentList{ }}; battery = new Battery("battery"){{ - powerCapacity = 320f; + consumes.powerBuffered(320f, 1f); }}; batteryLarge = new Battery("battery-large"){{ size = 3; - powerCapacity = 2000f; + consumes.powerBuffered(2000f, 1f); }}; powerNode = new PowerNode("power-node"){{ diff --git a/core/src/io/anuke/mindustry/content/blocks/TurretBlocks.java b/core/src/io/anuke/mindustry/content/blocks/TurretBlocks.java index 4249c80b19..053339535b 100644 --- a/core/src/io/anuke/mindustry/content/blocks/TurretBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/TurretBlocks.java @@ -92,8 +92,8 @@ public class TurretBlocks extends BlockList implements ContentList{ recoil = 2f; reload = 120f; cooldown = 0.03f; - powerUsed = 20f; - powerCapacity = 60f; + powerUsed = 1 / 3f; + consumes.powerBuffered(60f); shootShake = 2f; shootEffect = ShootFx.lancerLaserShoot; smokeEffect = ShootFx.lancerLaserShootSmoke; @@ -111,8 +111,8 @@ public class TurretBlocks extends BlockList implements ContentList{ shootShake = 0f; shootCone = 40f; rotatespeed = 8f; - powerUsed = 3f; - powerCapacity = 30f; + powerUsed = 1f / 3f; + consumes.powerBuffered(30f); range = 50f; shootEffect = ShootFx.lightningShoot; heatColor = Color.RED; @@ -249,8 +249,8 @@ public class TurretBlocks extends BlockList implements ContentList{ recoil = 4f; size = 4; shootShake = 2f; - powerUsed = 60f; - powerCapacity = 120f; + powerUsed = 0.5f; + consumes.powerBuffered(120f); range = 160f; reload = 200f; firingMoveFract = 0.1f; diff --git a/core/src/io/anuke/mindustry/content/blocks/UpgradeBlocks.java b/core/src/io/anuke/mindustry/content/blocks/UpgradeBlocks.java index d9c6d2d98d..359033c45e 100644 --- a/core/src/io/anuke/mindustry/content/blocks/UpgradeBlocks.java +++ b/core/src/io/anuke/mindustry/content/blocks/UpgradeBlocks.java @@ -9,52 +9,53 @@ public class UpgradeBlocks extends BlockList{ @Override public void load(){ + alphaPad = new MechPad("alpha-mech-pad"){{ mech = Mechs.alpha; size = 2; - powerCapacity = 50f; + consumes.powerBuffered(50f); }}; deltaPad = new MechPad("delta-mech-pad"){{ mech = Mechs.delta; size = 2; - powerCapacity = 70f; + consumes.powerBuffered(70f); }}; tauPad = new MechPad("tau-mech-pad"){{ mech = Mechs.tau; size = 2; - powerCapacity = 100f; + consumes.powerBuffered(100f); }}; omegaPad = new MechPad("omega-mech-pad"){{ mech = Mechs.omega; size = 3; - powerCapacity = 120f; + consumes.powerBuffered(120f); }}; dartPad = new MechPad("dart-ship-pad"){{ mech = Mechs.dart; size = 2; - powerCapacity = 50f; + consumes.powerBuffered(50f); }}; javelinPad = new MechPad("javelin-ship-pad"){{ mech = Mechs.javelin; size = 2; - powerCapacity = 80f; + consumes.powerBuffered(80f); }}; tridentPad = new MechPad("trident-ship-pad"){{ mech = Mechs.trident; size = 2; - powerCapacity = 100f; + consumes.powerBuffered(100f); }}; glaivePad = new MechPad("glaive-ship-pad"){{ mech = Mechs.glaive; size = 3; - powerCapacity = 120f; + consumes.powerBuffered(120f); }}; } } diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java index 3f2fc94c94..cff7944281 100644 --- a/core/src/io/anuke/mindustry/core/Renderer.java +++ b/core/src/io/anuke/mindustry/core/Renderer.java @@ -297,7 +297,7 @@ public class Renderer implements ApplicationListener{ public void clampScale(){ float s = io.anuke.arc.scene.ui.layout.Unit.dp.scl(1f); - targetscale = Mathf.clamp(targetscale, Math.round(s * 2), Math.round(s * 6)); + targetscale = Mathf.clamp(targetscale, s * 2.5f, Math.round(s * 7)); } public void takeMapScreenshot(){ diff --git a/core/src/io/anuke/mindustry/entities/traits/BuilderTrait.java b/core/src/io/anuke/mindustry/entities/traits/BuilderTrait.java index 153d21ea37..9a3b6e174d 100644 --- a/core/src/io/anuke/mindustry/entities/traits/BuilderTrait.java +++ b/core/src/io/anuke/mindustry/entities/traits/BuilderTrait.java @@ -310,7 +310,7 @@ public interface BuilderTrait extends Entity{ return; } - Draw.color(Palette.accent); + Lines.stroke(1f, Palette.accent); float focusLen = 3.8f + Mathf.absin(Time.time(), 1.1f, 0.6f); float px = unit.x + Angles.trnsx(unit.rotation, focusLen); float py = unit.y + Angles.trnsy(unit.rotation, focusLen); diff --git a/core/src/io/anuke/mindustry/input/DesktopInput.java b/core/src/io/anuke/mindustry/input/DesktopInput.java index b829e384a8..034a896272 100644 --- a/core/src/io/anuke/mindustry/input/DesktopInput.java +++ b/core/src/io/anuke/mindustry/input/DesktopInput.java @@ -67,6 +67,7 @@ public class DesktopInput extends InputHandler{ @Override public void drawOutlined(){ + Lines.stroke(1f); int cursorX = tileX(Core.input.mouseX()); int cursorY = tileY(Core.input.mouseY()); diff --git a/core/src/io/anuke/mindustry/input/MobileInput.java b/core/src/io/anuke/mindustry/input/MobileInput.java index b8fc6c00cd..4ac8d2f761 100644 --- a/core/src/io/anuke/mindustry/input/MobileInput.java +++ b/core/src/io/anuke/mindustry/input/MobileInput.java @@ -47,6 +47,7 @@ public class MobileInput extends InputHandler implements GestureListener{ //gesture data private Vector2 vector = new Vector2(); + private float lastDistance = -1f; private boolean canPan; /** Set of completed guides. */ private ObjectSet guides = new ObjectSet<>(); @@ -691,8 +692,11 @@ public class MobileInput extends InputHandler implements GestureListener{ @Override public boolean zoom(float initialDistance, float distance){ - float amount = (distance > initialDistance ? 0.1f : -0.1f) * Time.delta(); + if(lastDistance == -1) lastDistance = initialDistance; + + float amount = (distance > lastDistance ? 0.07f : -0.07f) * Time.delta(); renderer.scaleCamera(io.anuke.arc.scene.ui.layout.Unit.dp.scl(amount)); + lastDistance = distance; return true; } diff --git a/core/src/io/anuke/mindustry/io/SaveIO.java b/core/src/io/anuke/mindustry/io/SaveIO.java index 45f1e24303..2248d056c4 100644 --- a/core/src/io/anuke/mindustry/io/SaveIO.java +++ b/core/src/io/anuke/mindustry/io/SaveIO.java @@ -13,6 +13,7 @@ import java.util.zip.InflaterInputStream; import static io.anuke.mindustry.Vars.*; +//TODO load backup meta if possible public class SaveIO{ public static final IntArray breakingVersions = IntArray.with(47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 63); public static final IntMap versions = new IntMap<>(); @@ -65,9 +66,7 @@ public class SaveIO{ public static boolean isSaveValid(DataInputStream stream){ try{ - int version = stream.readInt(); - SaveFileVersion ver = versions.get(version); - ver.getData(stream); + getData(stream); return true; }catch(Exception e){ e.printStackTrace(); diff --git a/core/src/io/anuke/mindustry/maps/generation/FortressGenerator.java b/core/src/io/anuke/mindustry/maps/generation/FortressGenerator.java index cdf72427f6..0b122d1f36 100644 --- a/core/src/io/anuke/mindustry/maps/generation/FortressGenerator.java +++ b/core/src/io/anuke/mindustry/maps/generation/FortressGenerator.java @@ -180,7 +180,7 @@ public class FortressGenerator{ Block block = tile.block(); if(block instanceof PowerTurret){ - tile.entity.power.amount = block.powerCapacity; + tile.entity.power.satisfaction = 1.0f; }else if(block instanceof ItemTurret){ ItemTurret turret = (ItemTurret)block; AmmoType[] type = turret.getAmmoTypes(); diff --git a/core/src/io/anuke/mindustry/world/BaseBlock.java b/core/src/io/anuke/mindustry/world/BaseBlock.java index 22b60c1936..835f764760 100644 --- a/core/src/io/anuke/mindustry/world/BaseBlock.java +++ b/core/src/io/anuke/mindustry/world/BaseBlock.java @@ -26,12 +26,11 @@ public abstract class BaseBlock extends MappableContent{ public boolean outputsLiquid = false; public boolean singleLiquid = true; public boolean consumesPower = true; - public boolean outputsPower; + public boolean outputsPower = false; public int itemCapacity = 10; public float liquidCapacity = 10f; public float liquidFlowFactor = 4.9f; - public float powerCapacity = 10f; public Consumers consumes = new Consumers(); public Producers produces = new Producers(); @@ -40,6 +39,10 @@ public abstract class BaseBlock extends MappableContent{ return true; } + public float getPowerProduction(Tile tile){ + return 0f; + } + /**Returns the amount of items this block can accept.*/ public int acceptStack(Item item, int amount, Tile tile, Unit source){ if(acceptItem(item, tile, tile) && hasItems && (source == null || source.getTeam() == tile.getTeam())){ @@ -98,19 +101,6 @@ public abstract class BaseBlock extends MappableContent{ tile.entity.liquids.add(liquid, amount); } - public boolean acceptPower(Tile tile, Tile source, float amount){ - return true; - } - - /**Returns how much power is accepted.*/ - public float addPower(Tile tile, float amount){ - float canAccept = Math.min(powerCapacity - tile.entity.power.amount, amount); - - tile.entity.power.amount += canAccept; - - return canAccept; - } - public void tryDumpLiquid(Tile tile, Liquid liquid){ Array proximity = tile.entity.proximity(); int dump = tile.getDump(); diff --git a/core/src/io/anuke/mindustry/world/Block.java b/core/src/io/anuke/mindustry/world/Block.java index f9136c7974..561eb9e90e 100644 --- a/core/src/io/anuke/mindustry/world/Block.java +++ b/core/src/io/anuke/mindustry/world/Block.java @@ -28,6 +28,7 @@ import io.anuke.mindustry.graphics.Palette; import io.anuke.mindustry.type.ContentType; import io.anuke.mindustry.type.Item; import io.anuke.mindustry.type.ItemStack; +import io.anuke.mindustry.world.consumers.ConsumePower; import io.anuke.mindustry.world.meta.*; import static io.anuke.mindustry.Vars.*; @@ -179,6 +180,14 @@ public class Block extends BaseBlock { return out; } + protected float getProgressIncrease(TileEntity entity, float baseTime){ + float progressIncrease = 1f / baseTime * entity.delta(); + if(hasPower){ + progressIncrease *= entity.power.satisfaction; // Reduced increase in case of low power + } + return progressIncrease; + } + public boolean isLayer(Tile tile){ return true; } @@ -321,7 +330,7 @@ public class Block extends BaseBlock { consumes.forEach(cons -> cons.display(stats)); - if(hasPower) stats.add(BlockStat.powerCapacity, powerCapacity, StatUnit.powerUnits); + // Note: Power stats are added by the consumers. if(hasLiquids) stats.add(BlockStat.liquidCapacity, liquidCapacity, StatUnit.liquidUnits); if(hasItems) stats.add(BlockStat.itemCapacity, itemCapacity, StatUnit.items); } @@ -379,8 +388,8 @@ public class Block extends BaseBlock { explosiveness += tile.entity.liquids.sum((liquid, amount) -> liquid.flammability * amount / 2f); } - if(hasPower){ - power += tile.entity.power.amount; + if(consumes.has(ConsumePower.class) && consumes.get(ConsumePower.class).isBuffered){ + power += tile.entity.power.satisfaction * consumes.get(ConsumePower.class).powerCapacity; } if(hasLiquids){ @@ -529,4 +538,4 @@ public class Block extends BaseBlock { "entity.graph", tile.entity.power != null && tile.entity.power.graph != null ? tile.entity.power.graph.getID() : null ); } -} \ No newline at end of file +} diff --git a/core/src/io/anuke/mindustry/world/blocks/BlockPart.java b/core/src/io/anuke/mindustry/world/blocks/BlockPart.java index fdb3e34561..476829489e 100644 --- a/core/src/io/anuke/mindustry/world/blocks/BlockPart.java +++ b/core/src/io/anuke/mindustry/world/blocks/BlockPart.java @@ -59,22 +59,6 @@ public class BlockPart extends Block{ block.handleLiquid(tile.getLinked(), source, liquid, amount); } - @Override - public float addPower(Tile tile, float amount){ - Block block = linked(tile); - if(block.hasPower){ - return block.addPower(tile.getLinked(), amount); - }else{ - return amount; - } - } - - @Override - public boolean acceptPower(Tile tile, Tile from, float amount){ - Block block = linked(tile); - return block.hasPower && block.acceptPower(tile.getLinked(), from, amount); - } - private Block linked(Tile tile){ return tile.getLinked().block(); } diff --git a/core/src/io/anuke/mindustry/world/blocks/defense/ForceProjector.java b/core/src/io/anuke/mindustry/world/blocks/defense/ForceProjector.java index bf68358ca6..e0a3a2c3e1 100644 --- a/core/src/io/anuke/mindustry/world/blocks/defense/ForceProjector.java +++ b/core/src/io/anuke/mindustry/world/blocks/defense/ForceProjector.java @@ -21,6 +21,7 @@ import io.anuke.mindustry.graphics.Palette; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter; +import io.anuke.mindustry.world.consumers.ConsumePower; import io.anuke.mindustry.world.meta.BlockStat; import io.anuke.mindustry.world.meta.StatUnit; @@ -40,9 +41,12 @@ public class ForceProjector extends Block { protected float cooldownNormal = 1.75f; protected float cooldownLiquid = 1.5f; protected float cooldownBrokenBase = 0.35f; + protected float basePowerDraw = 0.2f; protected float powerDamage = 0.1f; + protected final ConsumeForceProjectorPower consumePower; protected TextureRegion topRegion; + public ForceProjector(String name) { super(name); update = true; @@ -50,9 +54,10 @@ public class ForceProjector extends Block { hasPower = true; canOverdrive = false; hasLiquids = true; - powerCapacity = 60f; hasItems = true; consumes.add(new ConsumeLiquidFilter(liquid -> liquid.temperature <= 0.5f && liquid.flammability < 0.1f, 0.1f)).optional(true).update(false); + consumePower = new ConsumeForceProjectorPower(60f, 60f); + consumes.add(consumePower); } @Override @@ -65,6 +70,7 @@ public class ForceProjector extends Block { public void setStats(){ super.setStats(); + stats.add(BlockStat.powerUse, basePowerDraw * 60f, StatUnit.powerSecond); stats.add(BlockStat.powerDamage, powerDamage, StatUnit.powerUnits); } @@ -90,15 +96,27 @@ public class ForceProjector extends Block { Effects.effect(BlockFx.reactorsmoke, tile.drawx() + Mathf.range(tilesize/2f), tile.drawy() + Mathf.range(tilesize/2f)); } - if(!entity.cons.valid() && !cheat){ + // Use Cases: + // - There is enough power in the buffer, and there are no shots fired => Draw base power and keep shield up + // - There is enough power in the buffer, but not enough power to cope for shots being fired => Draw all power and break shield + // - There is enough power in the buffer and enough power to cope for shots being fired => Draw base power + additional power based on shots absorbed + // - There is not enough base power in the buffer => Draw all power and break shield + // - The generator is in the AI base and uses cheat mode => Only draw power from shots being absorbed + + float relativePowerDraw = 0.0f; + if(!cheat){ + relativePowerDraw = basePowerDraw / consumePower.powerCapacity; + } + + if(entity.power.satisfaction < relativePowerDraw){ entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.15f); + entity.power.satisfaction = .0f; if(entity.warmup <= 0.09f){ entity.broken = true; } }else{ entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.1f); - float powerUse = Math.min(powerDamage * entity.delta() * (1f + entity.buildup / breakage), powerCapacity); - entity.power.amount -= powerUse; + entity.power.satisfaction -= Math.min(entity.power.satisfaction, relativePowerDraw); } if(entity.buildup > 0){ @@ -133,12 +151,12 @@ public class ForceProjector extends Block { if(trait.canBeAbsorbed() && trait.getTeam() != tile.getTeam() && isInsideHexagon(trait.getX(), trait.getY(), realRadius * 2f, tile.drawx(), tile.drawy())){ trait.absorb(); Effects.effect(BulletFx.absorb, trait); - float hit = trait.getShieldDamage()*powerDamage; + float relativeDamagePowerDraw = trait.getShieldDamage() * powerDamage / consumePower.powerCapacity; entity.hit = 1f; - entity.power.amount -= Math.min(hit, entity.power.amount); - if(entity.power.amount <= 0.0001f){ - entity.buildup += trait.getShieldDamage() * entity.warmup*2f; + entity.power.satisfaction -= Math.min(relativeDamagePowerDraw, entity.power.satisfaction); + if(entity.power.satisfaction <= 0.0001f){ + entity.buildup += trait.getShieldDamage() * entity.warmup * 2f; } entity.buildup += trait.getShieldDamage() * entity.warmup; } @@ -245,4 +263,14 @@ public class ForceProjector extends Block { return shieldGroup; } } + + public class ConsumeForceProjectorPower extends ConsumePower{ + public ConsumeForceProjectorPower(float powerCapacity, float ticksToFill){ + super(powerCapacity / ticksToFill, 0.0f, powerCapacity, true); + } + @Override + public boolean valid(Block block, TileEntity entity){ + return entity.power.satisfaction >= basePowerDraw / powerCapacity && super.valid(block, entity); + } + } } diff --git a/core/src/io/anuke/mindustry/world/blocks/defense/MendProjector.java b/core/src/io/anuke/mindustry/world/blocks/defense/MendProjector.java index f8c64ffbb0..a945cfdf68 100644 --- a/core/src/io/anuke/mindustry/world/blocks/defense/MendProjector.java +++ b/core/src/io/anuke/mindustry/world/blocks/defense/MendProjector.java @@ -83,7 +83,7 @@ public class MendProjector extends Block{ other = other.target(); if(other.getTeamID() == tile.getTeamID() && !healed.contains(other.pos()) && other.entity != null && other.entity.health < other.entity.maxHealth()){ - other.entity.healBy(other.entity.maxHealth() * (healPercent + entity.phaseHeat*phaseBoost)/100f); + other.entity.healBy(other.entity.maxHealth() * (healPercent + entity.phaseHeat*phaseBoost)/100f * entity.power.satisfaction); Effects.effect(BlockFx.healBlockFull, Tmp.c1.set(color).lerp(phase, entity.phaseHeat), other.drawx(), other.drawy(), other.block().size); healed.add(other.pos()); } diff --git a/core/src/io/anuke/mindustry/world/blocks/defense/OverdriveProjector.java b/core/src/io/anuke/mindustry/world/blocks/defense/OverdriveProjector.java index 1426dc9bc8..100856eeca 100644 --- a/core/src/io/anuke/mindustry/world/blocks/defense/OverdriveProjector.java +++ b/core/src/io/anuke/mindustry/world/blocks/defense/OverdriveProjector.java @@ -67,7 +67,7 @@ public class OverdriveProjector extends Block{ if(entity.charge >= reload){ float realRange = range + entity.phaseHeat * phaseRangeBoost; - float realBoost = speedBoost + entity.phaseHeat*speedBoostPhase; + float realBoost = (speedBoost + entity.phaseHeat*speedBoostPhase) * entity.power.satisfaction; Effects.effect(BlockFx.overdriveWave, Tmp.c1.set(color).lerp(phase, entity.phaseHeat), tile.drawx(), tile.drawy(), realRange); entity.charge = 0f; diff --git a/core/src/io/anuke/mindustry/world/blocks/defense/turrets/PowerTurret.java b/core/src/io/anuke/mindustry/world/blocks/defense/turrets/PowerTurret.java index 1ef8378252..3f8f6682fd 100644 --- a/core/src/io/anuke/mindustry/world/blocks/defense/turrets/PowerTurret.java +++ b/core/src/io/anuke/mindustry/world/blocks/defense/turrets/PowerTurret.java @@ -6,6 +6,7 @@ import io.anuke.mindustry.world.meta.BlockStat; import io.anuke.mindustry.world.meta.StatUnit; public abstract class PowerTurret extends CooledTurret{ + /** The percentage of power which will be used per shot. */ protected float powerUsed = 0.5f; protected AmmoType shootType; @@ -23,13 +24,15 @@ public abstract class PowerTurret extends CooledTurret{ @Override public boolean hasAmmo(Tile tile){ - return tile.entity.power.amount >= powerUsed; + // Allow shooting as long as the turret is at least at 50% power + return tile.entity.power.satisfaction >= powerUsed; } @Override public AmmoType useAmmo(Tile tile){ if(tile.isEnemyCheat()) return shootType; - tile.entity.power.amount -= powerUsed; + // Make sure that power can not go negative in case of threading issues or similar + tile.entity.power.satisfaction -= Math.min(powerUsed, tile.entity.power.satisfaction); return shootType; } diff --git a/core/src/io/anuke/mindustry/world/blocks/distribution/LiquidBridge.java b/core/src/io/anuke/mindustry/world/blocks/distribution/LiquidBridge.java index f6b563bb1b..ba8350c5d4 100644 --- a/core/src/io/anuke/mindustry/world/blocks/distribution/LiquidBridge.java +++ b/core/src/io/anuke/mindustry/world/blocks/distribution/LiquidBridge.java @@ -31,7 +31,11 @@ public class LiquidBridge extends ItemBridge{ tryDumpLiquid(tile, entity.liquids.current()); }else{ if(entity.cons.valid()){ - entity.uptime = Mathf.lerpDelta(entity.uptime, 1f, 0.04f); + float alpha = 0.04f; + if(hasPower){ + alpha *= entity.power.satisfaction; // Exceed boot time unless power is at max. + } + entity.uptime = Mathf.lerpDelta(entity.uptime, 1f, alpha); }else{ entity.uptime = Mathf.lerpDelta(entity.uptime, 0f, 0.02f); } diff --git a/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java b/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java index 831e3c53c2..13a15f3cad 100644 --- a/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java +++ b/core/src/io/anuke/mindustry/world/blocks/distribution/MassDriver.java @@ -28,6 +28,7 @@ import io.anuke.mindustry.graphics.Palette; import io.anuke.mindustry.type.Item; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.consumers.ConsumePower; import io.anuke.mindustry.world.meta.BlockStat; import io.anuke.mindustry.world.meta.StatUnit; @@ -48,6 +49,7 @@ public class MassDriver extends Block{ protected Effect smokeEffect = ShootFx.shootBigSmoke2; protected Effect recieveEffect = BlockFx.mineBig; protected float shake = 3f; + protected final static float powerPercentageUsed = 1.0f; protected TextureRegion turretRegion; public MassDriver(String name){ @@ -58,6 +60,8 @@ public class MassDriver extends Block{ hasItems = true; layer = Layer.turret; hasPower = true; + consumes.powerBuffered(30f); + consumes.require(ConsumePower.class); } @Remote(targets = Loc.both, called = Loc.server, forward = true) @@ -77,7 +81,8 @@ public class MassDriver extends Block{ MassDriverEntity other = target.entity(); entity.reload = 1f; - entity.power.amount = 0f; + + entity.power.satisfaction -= Math.min(entity.power.satisfaction, powerPercentageUsed); DriverBulletData data = Pools.obtain(DriverBulletData.class, DriverBulletData::new); data.from = entity; @@ -125,7 +130,7 @@ public class MassDriver extends Block{ public void setStats(){ super.setStats(); - stats.add(BlockStat.powerShot, powerCapacity, StatUnit.powerUnits); + stats.add(BlockStat.powerShot, consumes.get(ConsumePower.class).powerCapacity * powerPercentageUsed, StatUnit.powerUnits); } @Override @@ -164,8 +169,8 @@ public class MassDriver extends Block{ entity.rotation = Mathf.slerpDelta(entity.rotation, tile.angleTo(waiter), rotateSpeed); }else if(tile.entity.items.total() >= minDistribute && - linkValid(tile) && //only fire when at least at half-capacity and power - tile.entity.power.amount >= powerCapacity * 0.8f && + linkValid(tile) && //only fire when at 100% power capacity + tile.entity.power.satisfaction >= powerPercentageUsed && link.block().itemCapacity - link.entity.items.total() >= minDistribute && entity.reload <= 0.0001f){ MassDriverEntity other = link.entity(); diff --git a/core/src/io/anuke/mindustry/world/blocks/power/BurnerGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/BurnerGenerator.java index 91051e8a85..0441f2a876 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/BurnerGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/BurnerGenerator.java @@ -6,7 +6,7 @@ import io.anuke.mindustry.type.Liquid; public class BurnerGenerator extends ItemLiquidGenerator{ public BurnerGenerator(String name){ - super(name); + super(InputType.LiquidsAndItems, name); } @Override diff --git a/core/src/io/anuke/mindustry/world/blocks/power/DecayGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/DecayGenerator.java index 225efd98c5..678da93587 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/DecayGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/DecayGenerator.java @@ -1,11 +1,12 @@ package io.anuke.mindustry.world.blocks.power; import io.anuke.mindustry.type.Item; +import io.anuke.mindustry.type.Liquid; -public class DecayGenerator extends ItemGenerator{ +public class DecayGenerator extends ItemLiquidGenerator{ public DecayGenerator(String name){ - super(name); + super(InputType.ItemsOnly, name); hasItems = true; hasLiquids = false; } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/FusionReactor.java b/core/src/io/anuke/mindustry/world/blocks/power/FusionReactor.java index efd00fbe74..22310893d2 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/FusionReactor.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/FusionReactor.java @@ -9,13 +9,13 @@ import io.anuke.arc.math.Mathf; import io.anuke.arc.util.Time; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.blocks.production.GenericCrafter.GenericCrafterEntity; -import io.anuke.mindustry.world.meta.BlockStat; -import io.anuke.mindustry.world.meta.StatUnit; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; public class FusionReactor extends PowerGenerator{ protected int plasmas = 4; - protected float maxPowerProduced = 2f; protected float warmupSpeed = 0.001f; protected Color plasma1 = Color.valueOf("ffd06b"), plasma2 = Color.valueOf("ff361b"); @@ -25,33 +25,27 @@ public class FusionReactor extends PowerGenerator{ super(name); hasPower = true; hasLiquids = true; - powerCapacity = 100f; + powerProduction = 2.0f; liquidCapacity = 30f; hasItems = true; } - @Override - public void setStats(){ - super.setStats(); - - stats.add(BlockStat.basePowerGeneration, maxPowerProduced * 60f, StatUnit.powerSecond); - } - @Override public void update(Tile tile){ FusionReactorEntity entity = tile.entity(); + float increaseOrDecrease = 1.0f; if(entity.cons.valid()){ entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, warmupSpeed); }else{ entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.01f); + increaseOrDecrease = -1.0f; } - float powerAdded = Math.min(powerCapacity - entity.power.amount, maxPowerProduced * Mathf.pow(entity.warmup, 4f) * Time.delta()); - entity.power.amount += powerAdded; - entity.totalProgress += entity.warmup * Time.delta(); + float efficiencyAdded = Mathf.pow(entity.warmup, 4f) * Time.delta(); + entity.productionEfficiency = Mathf.clamp(entity.productionEfficiency + efficiencyAdded * increaseOrDecrease); - tile.entity.power.graph.update(); + super.update(tile); } @Override @@ -95,7 +89,7 @@ public class FusionReactor extends PowerGenerator{ Draw.rect(name + "-top", tile.drawx(), tile.drawy()); - Draw.color(ind1, ind2, entity.warmup + Mathf.absin(entity.totalProgress, 3f, entity.warmup * 0.5f)); + Draw.color(ind1, ind2, entity.warmup + Mathf.absin(entity.productionEfficiency, 3f, entity.warmup * 0.5f)); Draw.rect(name + "-light", tile.drawx(), tile.drawy()); Draw.color(); @@ -122,7 +116,19 @@ public class FusionReactor extends PowerGenerator{ //TODO catastrophic failure } - public static class FusionReactorEntity extends GenericCrafterEntity{ + public static class FusionReactorEntity extends GeneratorEntity{ + public float warmup; + @Override + public void write(DataOutput stream) throws IOException{ + super.write(stream); + stream.writeFloat(warmup); + } + + @Override + public void read(DataInput stream) throws IOException{ + super.read(stream); + warmup = stream.readFloat(); + } } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/ItemGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/ItemGenerator.java deleted file mode 100644 index e111b188fd..0000000000 --- a/core/src/io/anuke/mindustry/world/blocks/power/ItemGenerator.java +++ /dev/null @@ -1,126 +0,0 @@ -package io.anuke.mindustry.world.blocks.power; - -import io.anuke.arc.Core; -import io.anuke.arc.graphics.Color; -import io.anuke.arc.graphics.g2d.TextureRegion; -import io.anuke.mindustry.content.fx.BlockFx; -import io.anuke.mindustry.entities.TileEntity; -import io.anuke.mindustry.type.Item; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.consumers.ConsumeItemFilter; -import io.anuke.mindustry.world.meta.BlockStat; -import io.anuke.mindustry.world.meta.StatUnit; -import io.anuke.arc.entities.Effects; -import io.anuke.arc.entities.Effects.Effect; -import io.anuke.arc.util.Time; -import io.anuke.arc.graphics.g2d.Draw; -import io.anuke.arc.math.Mathf; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -import static io.anuke.mindustry.Vars.tilesize; - -public abstract class ItemGenerator extends PowerGenerator{ - protected float minItemEfficiency = 0.2f; - protected float powerOutput; - protected float itemDuration = 70f; - protected Effect generateEffect = BlockFx.generatespark, explodeEffect = - BlockFx.generatespark; - protected Color heatColor = Color.valueOf("ff9b59"); - protected TextureRegion topRegion; - - public ItemGenerator(String name){ - super(name); - hasItems = true; - - consumes.add(new ConsumeItemFilter(item -> getItemEfficiency(item) >= minItemEfficiency)).update(false).optional(true); - } - - @Override - public void load(){ - super.load(); - topRegion = Core.atlas.find(name + "-top"); - } - - @Override - public void setStats(){ - super.setStats(); - - stats.add(BlockStat.basePowerGeneration, powerOutput * 60f * 0.5f, StatUnit.powerSecond); - } - - @Override - public void draw(Tile tile){ - super.draw(tile); - - GeneratorEntity entity = tile.entity(); - - if(entity.generateTime > 0){ - Draw.color(heatColor); - float alpha = (entity.items.total() > 0 ? 1f : Mathf.clamp(entity.generateTime)); - alpha = alpha * 0.7f + Mathf.absin(Time.time(), 12f, 0.3f) * alpha; - Draw.alpha(alpha); - Draw.rect(topRegion, tile.drawx(), tile.drawy()); - Draw.reset(); - } - } - - @Override - public boolean acceptItem(Item item, Tile tile, Tile source){ - return getItemEfficiency(item) >= minItemEfficiency && tile.entity.items.total() < itemCapacity; - } - - @Override - public void update(Tile tile){ - ItemGeneratorEntity entity = tile.entity(); - - float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency; - - if(entity.generateTime <= 0f && entity.items.total() > 0){ - Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f)); - Item item = entity.items.take(); - entity.efficiency = getItemEfficiency(item); - entity.explosiveness = item.explosiveness; - entity.generateTime = 1f; - } - - entity.power.graph.update(); - - if(entity.generateTime > 0f){ - entity.generateTime -= 1f / itemDuration * entity.delta(); - entity.power.amount += maxPower; - entity.generateTime = Mathf.clamp(entity.generateTime); - - if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){ - //this block is run last so that in the event of a block destruction, no code relies on the block type - entity.damage(Mathf.random(8f)); - Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f)); - } - } - } - - protected abstract float getItemEfficiency(Item item); - - @Override - public TileEntity newEntity(){ - return new ItemGeneratorEntity(); - } - - public static class ItemGeneratorEntity extends GeneratorEntity{ - public float efficiency; - public float explosiveness; - - @Override - public void write(DataOutput stream) throws IOException{ - stream.writeFloat(efficiency); - } - - @Override - public void read(DataInput stream) throws IOException{ - efficiency = stream.readFloat(); - } - } - -} diff --git a/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java index e07e232130..d2d5106863 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/ItemLiquidGenerator.java @@ -1,107 +1,186 @@ package io.anuke.mindustry.world.blocks.power; +import io.anuke.arc.Core; +import io.anuke.arc.entities.Effects; +import io.anuke.arc.graphics.Color; +import io.anuke.arc.graphics.g2d.Draw; +import io.anuke.arc.graphics.g2d.TextureRegion; +import io.anuke.arc.math.Mathf; +import io.anuke.arc.util.Time; +import io.anuke.mindustry.content.fx.BlockFx; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.type.Item; import io.anuke.mindustry.type.Liquid; import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.consumers.ConsumeItemFilter; import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter; -import io.anuke.arc.entities.Effects; -import io.anuke.arc.graphics.g2d.Draw; -import io.anuke.arc.math.Mathf; import static io.anuke.mindustry.Vars.content; import static io.anuke.mindustry.Vars.tilesize; -public abstract class ItemLiquidGenerator extends ItemGenerator{ +/** + * Power generation block which can use items, liquids or both as input sources for power production. + * Liquids will take priority over items. + */ +public class ItemLiquidGenerator extends PowerGenerator{ + + protected float minItemEfficiency = 0.2f; + /** The time in number of ticks during which a single item will produce power. */ + protected float itemDuration = 70f; + protected float minLiquidEfficiency = 0.2f; - protected float powerPerLiquid = 0.13f; - /**Maximum liquid used per frame.*/ + /** Maximum liquid used per frame. */ protected float maxLiquidGenerate = 0.4f; - public ItemLiquidGenerator(String name){ - super(name); - hasLiquids = true; - liquidCapacity = 10f; + protected Effects.Effect generateEffect = BlockFx.generatespark; + protected Effects.Effect explodeEffect = BlockFx.generatespark; + protected Color heatColor = Color.valueOf("ff9b59"); + protected TextureRegion topRegion; - consumes.add(new ConsumeLiquidFilter(liquid -> getLiquidEfficiency(liquid) >= minLiquidEfficiency, 0.001f, true)).update(false).optional(true); + public enum InputType{ + ItemsOnly, + LiquidsOnly, + LiquidsAndItems + } + + public ItemLiquidGenerator(InputType inputType, String name){ + super(name); + this.hasItems = inputType != InputType.LiquidsOnly; + this.hasLiquids = inputType != InputType.ItemsOnly; + + if(hasItems){ + itemCapacity = 20; + consumes.add(new ConsumeItemFilter(item -> getItemEfficiency(item) >= minItemEfficiency)).update(false).optional(true); + } + + if(hasLiquids){ + liquidCapacity = 10f; + consumes.add(new ConsumeLiquidFilter(liquid -> getLiquidEfficiency(liquid) >= minLiquidEfficiency, 0.001f, true)).update(false).optional(true); + } } @Override - public void init(){ - super.init(); + public void load(){ + super.load(); + if(hasItems){ + topRegion = Core.atlas.find(name + "-top"); + } } + @Override public void update(Tile tile){ - ItemGeneratorEntity entity = tile.entity(); + ItemLiquidGeneratorEntity entity = tile.entity(); - entity.power.graph.update(); + // Note: Do not use this delta when calculating the amount of power or the power efficiency, but use it for resource consumption if necessary. + // Power amount is delta'd by PowerGraph class already. + float calculationDelta = entity.delta(); + + if(!entity.cons.valid()){ + entity.productionEfficiency = 0.0f; + return; + } Liquid liquid = null; for(Liquid other : content.liquids()){ - if(entity.liquids.get(other) >= 0.001f && getLiquidEfficiency(other) >= minLiquidEfficiency){ + if(hasLiquids && entity.liquids.get(other) >= 0.001f && getLiquidEfficiency(other) >= minLiquidEfficiency){ liquid = other; break; } } - //liquid takes priority over solids - if(liquid != null && entity.liquids.get(liquid) >= 0.001f && entity.cons.valid()){ - float powerPerLiquid = getLiquidEfficiency(liquid) * this.powerPerLiquid; - float used = Math.min(entity.liquids.get(liquid), maxLiquidGenerate * entity.delta()); - used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid); + if(hasLiquids && liquid != null && entity.liquids.get(liquid) >= 0.001f){ + float baseLiquidEfficiency = getLiquidEfficiency(liquid); + float maximumPossible = maxLiquidGenerate * calculationDelta; + float used = Math.min(entity.liquids.get(liquid) * calculationDelta, maximumPossible); entity.liquids.remove(liquid, used); - entity.power.amount += used * powerPerLiquid; + + // Note: 0.5 = 100%. PowerGraph will multiply this efficiency by two on its own. + entity.productionEfficiency = Mathf.clamp(baseLiquidEfficiency * used / maximumPossible); if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){ Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f)); } - }else if(entity.cons.valid()){ - - float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency; - + }else if(hasItems){ + // No liquids accepted or none supplied, try using items if accepted if(entity.generateTime <= 0f && entity.items.total() > 0){ Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f)); Item item = entity.items.take(); - entity.efficiency = getItemEfficiency(item); + entity.productionEfficiency = getItemEfficiency(item); entity.explosiveness = item.explosiveness; entity.generateTime = 1f; } if(entity.generateTime > 0f){ - entity.generateTime -= 1f / itemDuration * entity.delta(); - entity.power.amount += maxPower; - entity.generateTime = Mathf.clamp(entity.generateTime); + entity.generateTime -= Math.min(1f / itemDuration * entity.delta(), entity.generateTime); if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){ + //this block is run last so that in the event of a block destruction, no code relies on the block type entity.damage(Mathf.random(8f)); Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f)); } + }else{ + entity.productionEfficiency = 0.0f; } } + + super.update(tile); + } + + @Override + public boolean acceptItem(Item item, Tile tile, Tile source){ + return hasItems && getItemEfficiency(item) >= minItemEfficiency && tile.entity.items.total() < itemCapacity; + } + + @Override + public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){ + return hasLiquids && getLiquidEfficiency(liquid) >= minLiquidEfficiency && tile.entity.liquids.get(liquid) < liquidCapacity; } @Override public void draw(Tile tile){ super.draw(tile); - TileEntity entity = tile.entity(); + GeneratorEntity entity = tile.entity(); - Draw.color(entity.liquids.current().color); - Draw.alpha(entity.liquids.currentAmount() / liquidCapacity); - drawLiquidCenter(tile); - Draw.color(); - } + if(hasItems){ + if(entity.generateTime > 0){ + Draw.color(heatColor); + float alpha = (entity.items.total() > 0 ? 1f : Mathf.clamp(entity.generateTime)); + alpha = alpha * 0.7f + Mathf.absin(Time.time(), 12f, 0.3f) * alpha; + Draw.alpha(alpha); + Draw.rect(topRegion, tile.drawx(), tile.drawy()); + Draw.reset(); + } + } - @Override - public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){ - return getLiquidEfficiency(liquid) >= minLiquidEfficiency && tile.entity.liquids.get(liquid) < liquidCapacity; + if(hasLiquids){ + Draw.color(entity.liquids.current().color); + Draw.alpha(entity.liquids.currentAmount() / liquidCapacity); + drawLiquidCenter(tile); + Draw.color(); + } } public void drawLiquidCenter(Tile tile){ Draw.rect("blank", tile.drawx(), tile.drawy(), 2, 2); } - protected abstract float getLiquidEfficiency(Liquid liquid); + protected float getItemEfficiency(Item item){ + return 0.0f; + } + + protected float getLiquidEfficiency(Liquid liquid){ + return 0.0f; + } + + @Override + public TileEntity newEntity(){ + return new ItemLiquidGeneratorEntity(); + } + + public static class ItemLiquidGeneratorEntity extends GeneratorEntity{ + public float explosiveness; + } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java deleted file mode 100644 index e7d4256406..0000000000 --- a/core/src/io/anuke/mindustry/world/blocks/power/LiquidGenerator.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.anuke.mindustry.world.blocks.power; - -import io.anuke.mindustry.content.fx.BlockFx; -import io.anuke.mindustry.entities.TileEntity; -import io.anuke.mindustry.type.Liquid; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.blocks.power.ItemGenerator.ItemGeneratorEntity; -import io.anuke.mindustry.world.consumers.ConsumeLiquidFilter; -import io.anuke.arc.entities.Effects; -import io.anuke.arc.entities.Effects.Effect; -import io.anuke.arc.graphics.g2d.Draw; -import io.anuke.arc.math.Mathf; - -public abstract class LiquidGenerator extends PowerGenerator{ - protected float minEfficiency = 0.2f; - protected float powerPerLiquid; - /**Maximum liquid used per frame.*/ - protected float maxLiquidGenerate; - protected Effect generateEffect = BlockFx.generatespark; - - public LiquidGenerator(String name){ - super(name); - liquidCapacity = 30f; - hasLiquids = true; - } - - @Override - public void setStats(){ - consumes.add(new ConsumeLiquidFilter(liquid -> getEfficiency(liquid) >= minEfficiency, maxLiquidGenerate)).update(false); - super.setStats(); - } - - @Override - public void draw(Tile tile){ - super.draw(tile); - - TileEntity entity = tile.entity(); - - Draw.color(entity.liquids.current().color); - Draw.alpha(entity.liquids.total() / liquidCapacity); - drawLiquidCenter(tile); - Draw.color(); - } - - public void drawLiquidCenter(Tile tile){ - Draw.rect("blank", tile.drawx(), tile.drawy(), 2, 2); - } - - @Override - public void update(Tile tile){ - TileEntity entity = tile.entity(); - - if(entity.liquids.get(entity.liquids.current()) >= 0.001f){ - float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid; - float used = Math.min(entity.liquids.currentAmount(), maxLiquidGenerate * entity.delta()); - used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid); - - entity.liquids.remove(entity.liquids.current(), used); - entity.power.amount += used * powerPerLiquid; - - if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){ - Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f)); - } - } - - tile.entity.power.graph.update(); - } - - @Override - public boolean acceptLiquid(Tile tile, Tile source, Liquid liquid, float amount){ - return getEfficiency(liquid) >= minEfficiency && super.acceptLiquid(tile, source, liquid, amount); - } - - @Override - public TileEntity newEntity(){ - return new ItemGeneratorEntity(); - } - - /** - * Returns an efficiency value for the specified liquid. - * Greater efficiency means more power generation. - * If a liquid's efficiency is below {@link #minEfficiency}, it is not accepted. - */ - protected abstract float getEfficiency(Liquid liquid); -} diff --git a/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java index 87e5275cc9..bf65ea8bd5 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/LiquidHeatGenerator.java @@ -1,24 +1,27 @@ package io.anuke.mindustry.world.blocks.power; +import io.anuke.mindustry.content.Liquids; import io.anuke.mindustry.type.Liquid; import io.anuke.mindustry.world.meta.BlockStat; import io.anuke.mindustry.world.meta.StatUnit; -public class LiquidHeatGenerator extends LiquidGenerator{ +public class LiquidHeatGenerator extends ItemLiquidGenerator{ public LiquidHeatGenerator(String name){ - super(name); + super(InputType.LiquidsOnly, name); } @Override public void setStats(){ super.setStats(); - stats.add(BlockStat.basePowerGeneration, maxLiquidGenerate * powerPerLiquid * 60f * 0.5f, StatUnit.powerSecond); + stats.remove(BlockStat.basePowerGeneration); + // Right now, Lava is the only thing that can be used. + stats.add(BlockStat.basePowerGeneration, powerProduction * getLiquidEfficiency(Liquids.lava) / maxLiquidGenerate * 60f, StatUnit.powerSecond); } @Override - protected float getEfficiency(Liquid liquid){ + protected float getLiquidEfficiency(Liquid liquid){ return liquid.temperature - 0.5f; } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/NuclearReactor.java b/core/src/io/anuke/mindustry/world/blocks/power/NuclearReactor.java index 35b5e295b9..5f7345ddf7 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/NuclearReactor.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/NuclearReactor.java @@ -33,7 +33,6 @@ public class NuclearReactor extends PowerGenerator{ protected Color coolColor = new Color(1, 1, 1, 0f); protected Color hotColor = Color.valueOf("ff9575a3"); protected int fuelUseTime = 120; //time to consume 1 fuel - protected float powerMultiplier = 0.45f; //power per frame, depends on full capacity protected float heating = 0.013f; //heating per frame protected float coolantPower = 0.015f; //how much heat decreases per coolant unit protected float smokeThreshold = 0.3f; //threshold at which block starts smoking @@ -48,7 +47,6 @@ public class NuclearReactor extends PowerGenerator{ super(name); itemCapacity = 30; liquidCapacity = 50; - powerCapacity = 80f; hasItems = true; hasLiquids = true; @@ -67,7 +65,10 @@ public class NuclearReactor extends PowerGenerator{ public void setStats(){ super.setStats(); stats.add(BlockStat.inputLiquid, new LiquidFilterValue(liquid -> liquid.temperature <= 0.5f)); - stats.add(BlockStat.basePowerGeneration, powerMultiplier * 60f * 0.5f, StatUnit.powerSecond); + + stats.remove(BlockStat.basePowerGeneration); + // Display the power which will be produced at 50% efficiency + stats.add(BlockStat.basePowerGeneration, powerProduction * 60f * 0.5f, StatUnit.powerSecond); } @Override @@ -76,11 +77,11 @@ public class NuclearReactor extends PowerGenerator{ int fuel = entity.items.get(consumes.item()); float fullness = (float) fuel / itemCapacity; + entity.productionEfficiency = fullness / 2.0f; // Currently, efficiency of 0.5 = 100% if(fuel > 0){ entity.heat += fullness * heating * Math.min(entity.delta(), 4f); - entity.power.amount += powerMultiplier * fullness * entity.delta(); - entity.power.amount = Mathf.clamp(entity.power.amount, 0f, powerCapacity); + if(entity.timer.get(timerFuel, fuelUseTime)){ entity.items.remove(consumes.item(), 1); } @@ -115,7 +116,7 @@ public class NuclearReactor extends PowerGenerator{ if(entity.heat >= 0.999f){ entity.kill(); }else{ - tile.entity.power.graph.update(); + super.update(tile); } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java index d2704af457..b6902c6796 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerGenerator.java @@ -1,10 +1,20 @@ package io.anuke.mindustry.world.blocks.power; -import io.anuke.mindustry.entities.TileEntity; -import io.anuke.mindustry.world.meta.BlockFlag; import io.anuke.arc.collection.EnumSet; +import io.anuke.mindustry.entities.TileEntity; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.meta.BlockFlag; +import io.anuke.mindustry.world.meta.BlockStat; +import io.anuke.mindustry.world.meta.StatUnit; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; public class PowerGenerator extends PowerDistributor{ + /** The amount of power produced per tick in case of an efficiency of 1.0, which currently represents 200%. */ + protected float powerProduction; + public BlockStat generationType = BlockStat.basePowerGeneration; public PowerGenerator(String name){ super(name); @@ -12,6 +22,20 @@ public class PowerGenerator extends PowerDistributor{ flags = EnumSet.of(BlockFlag.producer); } + @Override + public void setStats(){ + super.setStats(); + // Divide power production by two since that is what is produced at an efficiency of 0.5, which currently represents 100% + stats.add(generationType, powerProduction * 60.0f / 2.0f, StatUnit.powerSecond); + } + + @Override + public float getPowerProduction(Tile tile){ + // While 0.5 efficiency currently reflects 100%, we do not need to multiply by any factor since powerProduction states the + // power which would be produced at 1.0 efficiency + return powerProduction * tile.entity().productionEfficiency; + } + @Override public boolean outputsItems(){ return false; @@ -24,5 +48,17 @@ public class PowerGenerator extends PowerDistributor{ public static class GeneratorEntity extends TileEntity{ public float generateTime; + /** The efficiency of the producer. Currently, an efficiency of 0.5 means 100% */ + public float productionEfficiency = 0.0f; + + @Override + public void write(DataOutput stream) throws IOException{ + stream.writeFloat(productionEfficiency); + } + + @Override + public void read(DataInput stream) throws IOException{ + productionEfficiency = stream.readFloat(); + } } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java index 90c24e1bf1..e25e8a649e 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java @@ -5,7 +5,11 @@ import io.anuke.arc.collection.Array; import io.anuke.arc.collection.IntSet; import io.anuke.arc.collection.ObjectSet; import io.anuke.arc.collection.Queue; +import io.anuke.arc.math.Mathf; import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.consumers.Consume; +import io.anuke.mindustry.world.consumers.ConsumePower; +import io.anuke.mindustry.world.consumers.Consumers; public class PowerGraph{ private final static Queue queue = new Queue<>(); @@ -15,6 +19,7 @@ public class PowerGraph{ private final ObjectSet producers = new ObjectSet<>(); private final ObjectSet consumers = new ObjectSet<>(); + private final ObjectSet batteries = new ObjectSet<>(); private final ObjectSet all = new ObjectSet<>(); private long lastFrameUpdated; @@ -29,71 +34,127 @@ public class PowerGraph{ return graphID; } + public float getPowerProduced(){ + float powerProduced = 0f; + for(Tile producer : producers){ + powerProduced += producer.block().getPowerProduction(producer) * producer.entity.delta(); + } + return powerProduced; + } + + public float getPowerNeeded(){ + float powerNeeded = 0f; + for(Tile consumer : consumers){ + Consumers consumes = consumer.block().consumes; + if(consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumes.get(ConsumePower.class); + if(otherConsumersAreValid(consumer, consumePower)){ + powerNeeded += consumePower.requestedPower(consumer.block(), consumer.entity) * consumer.entity.delta(); + } + } + } + return powerNeeded; + } + + public float getBatteryStored(){ + float totalAccumulator = 0f; + for(Tile battery : batteries){ + Consumers consumes = battery.block().consumes; + if(consumes.has(ConsumePower.class)){ + totalAccumulator += battery.entity.power.satisfaction * consumes.get(ConsumePower.class).powerCapacity; + } + } + return totalAccumulator; + } + + public float getBatteryCapacity(){ + float totalCapacity = 0f; + for(Tile battery : batteries){ + Consumers consumes = battery.block().consumes; + if(consumes.has(ConsumePower.class)){ + totalCapacity += consumes.get(ConsumePower.class).requestedPower(battery.block(), battery.entity) * battery.entity.delta(); + } + } + return totalCapacity; + } + + public float useBatteries(float needed){ + float stored = getBatteryStored(); + if(Mathf.isEqual(stored, 0f)){ return 0f; } + + float used = Math.min(stored, needed); + float consumedPowerPercentage = Math.min(1.0f, needed / stored); + for(Tile battery : batteries){ + Consumers consumes = battery.block().consumes; + if(consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumes.get(ConsumePower.class); + if(consumePower.powerCapacity > 0f){ + battery.entity.power.satisfaction = Math.max(0.0f, battery.entity.power.satisfaction - consumedPowerPercentage); + } + } + } + return used; + } + + public float chargeBatteries(float excess){ + float capacity = getBatteryCapacity(); + if(Mathf.isEqual(capacity, 0f)){ return 0f; } + + for(Tile battery : batteries){ + Consumers consumes = battery.block().consumes; + if(consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumes.get(ConsumePower.class); + if(consumePower.powerCapacity > 0f){ + float additionalPowerPercentage = Math.min(1.0f, excess / consumePower.powerCapacity); + battery.entity.power.satisfaction = Math.min(1.0f, battery.entity.power.satisfaction + additionalPowerPercentage); + } + } + } + return Math.min(excess, capacity); + } + + public void distributePower(float needed, float produced){ + if(Mathf.isEqual(needed, 0f)){ return; } + + float coverage = Math.min(1, produced / needed); + for(Tile consumer : consumers){ + Consumers consumes = consumer.block().consumes; + if(consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumes.get(ConsumePower.class); + if(!otherConsumersAreValid(consumer, consumePower)){ + consumer.entity.power.satisfaction = 0.0f; // Only supply power if the consumer would get valid that way + }else{ + if(consumePower.isBuffered){ + // Add an equal percentage of power to all buffers, based on the global power coverage in this graph + float maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage * consumer.entity.delta(); + consumer.entity.power.satisfaction = Mathf.clamp(consumer.entity.power.satisfaction + maximumRate / consumePower.powerCapacity); + }else{ + consumer.entity.power.satisfaction = coverage; + } + } + } + } + } + public void update(){ - if(Core.graphics.getFrameId() == lastFrameUpdated || consumers.size == 0 || producers.size == 0){ + if(Core.graphics.getFrameId() == lastFrameUpdated || consumers.size == 0 && producers.size == 0 && batteries.size == 0){ return; } lastFrameUpdated = Core.graphics.getFrameId(); - boolean charge = false; + float powerNeeded = getPowerNeeded(); + float powerProduced = getPowerProduced(); - float totalInput = 0f; - float bufferInput = 0f; - for(Tile producer : producers){ - if(producer.block().consumesPower){ - bufferInput += producer.entity.power.amount; - }else{ - totalInput += producer.entity.power.amount; + if(!Mathf.isEqual(powerNeeded, powerProduced)){ + if(powerNeeded > powerProduced){ + powerProduced += useBatteries(powerNeeded - powerProduced); + }else if(powerProduced > powerNeeded){ + powerProduced -= chargeBatteries(powerProduced - powerNeeded); } } - float maxOutput = 0f; - float bufferOutput = 0f; - for(Tile consumer : consumers){ - if(consumer.block().outputsPower){ - bufferOutput += consumer.block().powerCapacity - consumer.entity.power.amount; - }else{ - maxOutput += consumer.block().powerCapacity - consumer.entity.power.amount; - } - } - - if(maxOutput < totalInput){ - charge = true; - } - - if(totalInput + bufferInput <= 0.0001f || maxOutput + bufferOutput <= 0.0001f){ - return; - } - - float bufferUsed; - if(charge){ - bufferUsed = Math.min((totalInput - maxOutput) / bufferOutput, 1f); - }else{ - bufferUsed = Math.min((maxOutput - totalInput) / bufferInput, 1f); - } - - float inputUsed = charge ? Math.min((maxOutput + bufferOutput) / totalInput, 1f) : 1f; - for(Tile producer : producers){ - if(producer.block().consumesPower){ - if(!charge){ - producer.entity.power.amount -= producer.entity.power.amount * bufferUsed; - } - continue; - } - producer.entity.power.amount -= producer.entity.power.amount * inputUsed; - } - - float outputSatisfied = charge ? 1f : Math.min((totalInput + bufferInput) / maxOutput, 1f); - for(Tile consumer : consumers){ - if(consumer.block().outputsPower){ - if(charge){ - consumer.entity.power.amount += (consumer.block().powerCapacity - consumer.entity.power.amount) * bufferUsed; - } - continue; - } - consumer.entity.power.amount += (consumer.block().powerCapacity - consumer.entity.power.amount) * outputSatisfied; - } + distributePower(powerNeeded, powerProduced); } public void add(PowerGraph graph){ @@ -106,22 +167,29 @@ public class PowerGraph{ tile.entity.power.graph = this; all.add(tile); - if(tile.block().outputsPower){ + if(tile.block().outputsPower && tile.block().consumesPower){ + batteries.add(tile); + }else if(tile.block().outputsPower){ producers.add(tile); - } - - if(tile.block().consumesPower){ + }else if(tile.block().consumesPower){ consumers.add(tile); } } public void clear(){ for(Tile other : all){ - if(other.entity != null && other.entity.power != null) other.entity.power.graph = null; + if(other.entity != null && other.entity.power != null){ + if(other.block().consumes.has(ConsumePower.class) && !other.block().consumes.get(ConsumePower.class).isBuffered){ + // Reset satisfaction to zero in case of direct consumer. There is no reason to clear power from buffered consumers. + other.entity.power.satisfaction = 0.0f; + } + other.entity.power.graph = null; + } } all.clear(); producers.clear(); consumers.clear(); + batteries.clear(); } public void reflow(Tile tile){ @@ -146,7 +214,7 @@ public class PowerGraph{ closedSet.clear(); for(Tile other : tile.block().getPowerConnections(tile, outArray1)){ - if(other.entity.power == null || other.entity.power.graph != null) continue; + if(other.entity.power == null || other.entity.power.graph != null){ continue; } PowerGraph graph = new PowerGraph(); queue.clear(); queue.addLast(other); @@ -161,17 +229,29 @@ public class PowerGraph{ } } } + // Update the graph once so direct consumers without any connected producer lose their power + graph.update(); } } + private boolean otherConsumersAreValid(Tile tile, Consume consumePower){ + for(Consume cons : tile.block().consumes.all()){ + if(cons != consumePower && !cons.isOptional() && !cons.valid(tile.block(), tile.entity())){ + return false; + } + } + return true; + } + @Override public String toString(){ return "PowerGraph{" + - "producers=" + producers + - ", consumers=" + consumers + - ", all=" + all + - ", lastFrameUpdated=" + lastFrameUpdated + - ", graphID=" + graphID + - '}'; + "producers=" + producers + + ", consumers=" + consumers + + ", batteries=" + batteries + + ", all=" + all + + ", lastFrameUpdated=" + lastFrameUpdated + + ", graphID=" + graphID + + '}'; } } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerNode.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerNode.java index 96e1af2cbd..c98c9fb952 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerNode.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerNode.java @@ -39,7 +39,6 @@ public class PowerNode extends PowerBlock{ super(name); expanded = true; layer = Layer.power; - powerCapacity = 5f; configurable = true; consumesPower = false; outputsPower = false; diff --git a/core/src/io/anuke/mindustry/world/blocks/power/SolarGenerator.java b/core/src/io/anuke/mindustry/world/blocks/power/SolarGenerator.java index 4d444587fd..3090c389f3 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/SolarGenerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/SolarGenerator.java @@ -1,34 +1,30 @@ package io.anuke.mindustry.world.blocks.power; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.meta.BlockStat; +import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.world.meta.StatUnit; -import io.anuke.arc.util.Time; import io.anuke.arc.collection.EnumSet; public class SolarGenerator extends PowerGenerator{ - /** - * power generated per frame - */ - protected float generation = 0.005f; public SolarGenerator(String name){ super(name); + // Remove the BlockFlag.producer flag to make this a lower priority target than other generators. flags = EnumSet.of(); } @Override public void setStats(){ super.setStats(); - - stats.add(BlockStat.basePowerGeneration, generation * 60f, StatUnit.powerSecond); + // Solar Generators don't really have an efficiency (yet), so for them 100% = 1.0f + stats.remove(generationType); + stats.add(generationType, powerProduction * 60.0f, StatUnit.powerSecond); } @Override - public void update(Tile tile){ - addPower(tile, generation * Time.delta()); - - tile.entity.power.graph.update(); + public TileEntity newEntity(){ + return new PowerGenerator.GeneratorEntity(){{ + productionEfficiency = 1.0f; + }}; } } diff --git a/core/src/io/anuke/mindustry/world/blocks/production/Drill.java b/core/src/io/anuke/mindustry/world/blocks/production/Drill.java index c6eca7b8a2..ba9b8dea28 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/Drill.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/Drill.java @@ -189,6 +189,9 @@ public class Drill extends Block{ if(entity.consumed(ConsumeLiquid.class) && !liquidRequired){ speed = liquidBoostIntensity; } + if(hasPower){ + speed *= entity.power.satisfaction; // Drill slower when not at full power + } entity.warmup = Mathf.lerpDelta(entity.warmup, speed, warmupSpeed); entity.progress += entity.delta() diff --git a/core/src/io/anuke/mindustry/world/blocks/production/Fracker.java b/core/src/io/anuke/mindustry/world/blocks/production/Fracker.java index 72dbca4222..efebd3c73f 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/Fracker.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/Fracker.java @@ -64,7 +64,7 @@ public class Fracker extends SolidPump{ if(entity.cons.valid() && entity.accumulator < itemUseTime){ super.update(tile); - entity.accumulator += entity.delta(); + entity.accumulator += entity.delta() * entity.power.satisfaction; }else{ tryDumpLiquid(tile, result); } diff --git a/core/src/io/anuke/mindustry/world/blocks/production/GenericCrafter.java b/core/src/io/anuke/mindustry/world/blocks/production/GenericCrafter.java index a7968ef8cf..4c1eef5ef7 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/GenericCrafter.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/GenericCrafter.java @@ -74,7 +74,7 @@ public class GenericCrafter extends Block{ if(entity.cons.valid() && tile.entity.items.get(output) < itemCapacity){ - entity.progress += 1f / craftTime * entity.delta(); + entity.progress += getProgressIncrease(entity, craftTime); entity.totalProgress += entity.delta(); entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f); diff --git a/core/src/io/anuke/mindustry/world/blocks/production/Incinerator.java b/core/src/io/anuke/mindustry/world/blocks/production/Incinerator.java index 973c20bada..d8fea6e766 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/Incinerator.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/Incinerator.java @@ -25,7 +25,8 @@ public class Incinerator extends Block{ update = true; solid = true; - consumes.power(0.05f); + // Incinerator has no speed which could be adjusted, so it will only operate fully powered for now + consumes.power(0.05f, 1.0f); } @Override diff --git a/core/src/io/anuke/mindustry/world/blocks/production/LiquidMixer.java b/core/src/io/anuke/mindustry/world/blocks/production/LiquidMixer.java index be88688468..00767ef304 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/LiquidMixer.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/LiquidMixer.java @@ -49,6 +49,9 @@ public class LiquidMixer extends LiquidBlock{ if(tile.entity.cons.valid()){ float use = Math.min(consumes.get(ConsumeLiquid.class).used() * entity.delta(), liquidCapacity - entity.liquids.get(outputLiquid)); + if(hasPower){ + use *= entity.power.satisfaction; // Produce less liquid if power is not maxed + } entity.accumulator += use; entity.liquids.add(outputLiquid, use); for(int i = 0; i < (int) (entity.accumulator / liquidPerItem); i++){ diff --git a/core/src/io/anuke/mindustry/world/blocks/production/PowerCrafter.java b/core/src/io/anuke/mindustry/world/blocks/production/PowerCrafter.java index 15fb12218b..4f3889eca5 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/PowerCrafter.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/PowerCrafter.java @@ -65,7 +65,7 @@ public class PowerCrafter extends Block{ GenericCrafterEntity entity = tile.entity(); if(entity.cons.valid()){ - entity.progress += 1f / craftTime * entity.delta(); + entity.progress += getProgressIncrease(entity, craftTime); entity.totalProgress += entity.delta(); } diff --git a/core/src/io/anuke/mindustry/world/blocks/production/PowerSmelter.java b/core/src/io/anuke/mindustry/world/blocks/production/PowerSmelter.java index 50c19d3c6d..a26a41d1a4 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/PowerSmelter.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/PowerSmelter.java @@ -115,7 +115,7 @@ public class PowerSmelter extends PowerBlock{ } } - entity.craftTime += entity.delta(); + entity.craftTime += entity.delta() * entity.power.satisfaction; if(entity.items.get(result) >= itemCapacity //output full || entity.heat <= minHeat //not burning diff --git a/core/src/io/anuke/mindustry/world/blocks/production/Pump.java b/core/src/io/anuke/mindustry/world/blocks/production/Pump.java index 305b337083..21b5c830c9 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/Pump.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/Pump.java @@ -96,6 +96,9 @@ public class Pump extends LiquidBlock{ if(tile.entity.cons.valid() && liquidDrop != null){ float maxPump = Math.min(liquidCapacity - tile.entity.liquids.total(), tiles * pumpAmount * tile.entity.delta()); + if(hasPower){ + maxPump *= tile.entity.power.satisfaction; // Produce slower if not at full power + } tile.entity.liquids.add(liquidDrop, maxPump); } diff --git a/core/src/io/anuke/mindustry/world/blocks/production/Separator.java b/core/src/io/anuke/mindustry/world/blocks/production/Separator.java index 1a706644e9..d1cdccf51a 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/Separator.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/Separator.java @@ -84,7 +84,7 @@ public class Separator extends Block{ entity.totalProgress += entity.warmup * entity.delta(); if(entity.cons.valid()){ - entity.progress += 1f / filterTime*entity.delta(); + entity.progress += getProgressIncrease(entity, filterTime); entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f); }else{ entity.warmup = Mathf.lerpDelta(entity.warmup, 0f, 0.02f); diff --git a/core/src/io/anuke/mindustry/world/blocks/production/SolidPump.java b/core/src/io/anuke/mindustry/world/blocks/production/SolidPump.java index 57fc27a42f..729fc05a90 100644 --- a/core/src/io/anuke/mindustry/world/blocks/production/SolidPump.java +++ b/core/src/io/anuke/mindustry/world/blocks/production/SolidPump.java @@ -77,7 +77,7 @@ public class SolidPump extends Pump{ } if(tile.entity.cons.valid() && typeLiquid(tile) < liquidCapacity - 0.001f){ - float maxPump = Math.min(liquidCapacity - typeLiquid(tile), pumpAmount * entity.delta() * fraction); + float maxPump = Math.min(liquidCapacity - typeLiquid(tile), pumpAmount * entity.delta() * fraction * entity.power.satisfaction); tile.entity.liquids.add(result, maxPump); entity.warmup = Mathf.lerpDelta(entity.warmup, 1f, 0.02f); if(Mathf.chance(entity.delta() * updateEffectChance)) diff --git a/core/src/io/anuke/mindustry/world/blocks/units/MechPad.java b/core/src/io/anuke/mindustry/world/blocks/units/MechPad.java index 75d48114ec..822b8d96bc 100644 --- a/core/src/io/anuke/mindustry/world/blocks/units/MechPad.java +++ b/core/src/io/anuke/mindustry/world/blocks/units/MechPad.java @@ -24,7 +24,6 @@ import io.anuke.mindustry.graphics.Shaders; import io.anuke.mindustry.type.Mech; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.consumers.ConsumePowerExact; import io.anuke.mindustry.world.meta.BlockStat; import java.io.DataInput; @@ -37,6 +36,7 @@ import static io.anuke.mindustry.Vars.tilesize; public class MechPad extends Block{ protected Mech mech; protected float buildTime = 60 * 5; + protected float requiredSatisfaction = 1f; protected TextureRegion openRegion; @@ -49,16 +49,9 @@ public class MechPad extends Block{ @Override public void init(){ - consumes.add(new ConsumePowerExact(powerCapacity * 0.8f)); super.init(); } - @Override - public void setStats(){ - super.setStats(); - stats.remove(BlockStat.powerUse); - } - @Override public boolean shouldConsume(Tile tile){ return false; @@ -66,10 +59,14 @@ public class MechPad extends Block{ @Remote(targets = Loc.both, called = Loc.server) public static void onMechFactoryTap(Player player, Tile tile){ - if(player == null || !checkValidTap(tile, player)) return; + if(player == null || !checkValidTap(tile, player) || !(tile.block() instanceof MechPad)) return; MechFactoryEntity entity = tile.entity(); - entity.power.amount = 0f; + MechPad pad = (MechPad)tile.block(); + + if(entity.power.satisfaction < pad.requiredSatisfaction) return; + + entity.power.satisfaction -= Math.min(entity.power.satisfaction, pad.requiredSatisfaction); player.beginRespawning(entity); } @@ -102,7 +99,7 @@ public class MechPad extends Block{ protected static boolean checkValidTap(Tile tile, Player player){ MechFactoryEntity entity = tile.entity(); - return Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f && + return Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f && Math.abs(player.y - tile.drawy()) <= tile.block().size * tilesize / 2f && entity.cons.valid() && entity.player == null; } diff --git a/core/src/io/anuke/mindustry/world/blocks/units/Reconstructor.java b/core/src/io/anuke/mindustry/world/blocks/units/Reconstructor.java index e81ab38efd..75d69c255b 100644 --- a/core/src/io/anuke/mindustry/world/blocks/units/Reconstructor.java +++ b/core/src/io/anuke/mindustry/world/blocks/units/Reconstructor.java @@ -34,7 +34,8 @@ import static io.anuke.mindustry.Vars.world; public class Reconstructor extends Block{ protected float departTime = 30f; protected float arriveTime = 40f; - protected float powerPerTeleport = 5f; + /** Stores the percentage of buffered power to be used upon teleporting. */ + protected float powerPerTeleport = 0.5f; protected Effect arriveEffect = Fx.spawn; protected TextureRegion openRegion; @@ -44,13 +45,14 @@ public class Reconstructor extends Block{ solidifes = true; hasPower = true; configurable = true; + consumes.powerBuffered(30f); } protected static boolean checkValidTap(Tile tile, ReconstructorEntity entity, Player player){ return validLink(tile, entity.link) && Math.abs(player.x - tile.drawx()) <= tile.block().size * tilesize / 2f && Math.abs(player.y - tile.drawy()) <= tile.block().size * tilesize / 2f && - entity.current == null && entity.power.amount >= ((Reconstructor) tile.block()).powerPerTeleport; + entity.current == null && entity.power.satisfaction >= ((Reconstructor) tile.block()).powerPerTeleport; } protected static boolean validLink(Tile tile, int position){ @@ -75,13 +77,13 @@ public class Reconstructor extends Block{ public static void reconstructPlayer(Player player, Tile tile){ ReconstructorEntity entity = tile.entity(); - if(!checkValidTap(tile, entity, player) || entity.power.amount < ((Reconstructor) tile.block()).powerPerTeleport) + if(!checkValidTap(tile, entity, player) || entity.power.satisfaction < ((Reconstructor) tile.block()).powerPerTeleport) return; entity.departing = true; entity.current = player; entity.solid = false; - entity.power.amount -= ((Reconstructor) tile.block()).powerPerTeleport; + entity.power.satisfaction -= Math.min(entity.power.satisfaction, ((Reconstructor) tile.block()).powerPerTeleport); entity.updateTime = 1f; entity.set(tile.drawx(), tile.drawy()); player.rotation = 90f; @@ -242,13 +244,13 @@ public class Reconstructor extends Block{ entity.updateTime -= Time.delta() / departTime; if(entity.updateTime <= 0f){ //no power? death. - if(other.power.amount < powerPerTeleport){ + if(other.power.satisfaction < powerPerTeleport){ entity.current.setDead(true); //entity.current.setRespawning(false); entity.current = null; return; } - other.power.amount -= powerPerTeleport; + other.power.satisfaction -= Math.min(other.power.satisfaction, powerPerTeleport); other.current = entity.current; other.departing = false; other.current.set(other.x, other.y); @@ -272,8 +274,8 @@ public class Reconstructor extends Block{ if(validLink(tile, entity.link)){ Tile other = world.tile(entity.link); - if(other.entity.power.amount >= powerPerTeleport && Units.anyEntities(tile, 4f, unit -> unit.getTeam() == entity.getTeam() && unit instanceof Player) && - entity.power.amount >= powerPerTeleport){ + if(other.entity.power.satisfaction >= powerPerTeleport && Units.anyEntities(tile, 4f, unit -> unit.getTeam() == entity.getTeam() && unit instanceof Player) && + entity.power.satisfaction >= powerPerTeleport){ entity.solid = false; stayOpen = true; } diff --git a/core/src/io/anuke/mindustry/world/blocks/units/RepairPoint.java b/core/src/io/anuke/mindustry/world/blocks/units/RepairPoint.java index 3a6a38cd61..f213ebf15a 100644 --- a/core/src/io/anuke/mindustry/world/blocks/units/RepairPoint.java +++ b/core/src/io/anuke/mindustry/world/blocks/units/RepairPoint.java @@ -18,7 +18,10 @@ import io.anuke.mindustry.graphics.Palette; import io.anuke.mindustry.graphics.Shapes; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.consumers.ConsumePower; import io.anuke.mindustry.world.meta.BlockFlag; +import io.anuke.mindustry.world.meta.BlockStat; +import io.anuke.mindustry.world.meta.StatUnit; public class RepairPoint extends Block{ private static Rectangle rect = new Rectangle(); @@ -27,6 +30,8 @@ public class RepairPoint extends Block{ protected float repairRadius = 50f; protected float repairSpeed = 0.3f; + protected float powerPerEvent = 0.06f; + protected ConsumePower consumePower; protected TextureRegion topRegion; @@ -38,8 +43,7 @@ public class RepairPoint extends Block{ layer = Layer.turret; layer2 = Layer.laser; hasPower = true; - powerCapacity = 20f; - consumes.power(0.06f); + consumePower = consumes.powerBuffered(20f); } @Override @@ -49,6 +53,12 @@ public class RepairPoint extends Block{ topRegion = Core.atlas.find(name + "-turret"); } + @Override + public void setStats(){ + super.setStats(); + stats.add(BlockStat.powerUse, powerPerEvent * 60f, StatUnit.powerSecond); + } + @Override public void drawSelect(Tile tile){ Draw.color(Palette.accent); @@ -84,16 +94,22 @@ public class RepairPoint extends Block{ public void update(Tile tile){ RepairPointEntity entity = tile.entity(); + boolean targetIsBeingRepaired = false; if(entity.target != null && (entity.target.isDead() || entity.target.dst(tile) > repairRadius || entity.target.health >= entity.target.maxHealth())){ entity.target = null; }else if(entity.target != null){ - entity.target.health += repairSpeed * Time.delta() * entity.strength; - entity.target.clampHealth(); - entity.rotation = Mathf.slerpDelta(entity.rotation, entity.angleTo(entity.target), 0.5f); + float relativeConsumption = powerPerEvent / consumePower.powerCapacity; + if(entity.power.satisfaction > 0.0f){ + entity.target.health += repairSpeed * Time.delta() * entity.strength * Mathf.clamp(entity.power.satisfaction / relativeConsumption); + entity.target.clampHealth(); + entity.rotation = Mathf.slerpDelta(entity.rotation, entity.angleTo(entity.target), 0.5f); + entity.power.satisfaction -= Math.min(entity.power.satisfaction, relativeConsumption); + targetIsBeingRepaired = true; + } } - if(entity.target != null && entity.cons.valid()){ + if(entity.target != null && targetIsBeingRepaired){ entity.strength = Mathf.lerpDelta(entity.strength, 1f, 0.08f * Time.delta()); }else{ entity.strength = Mathf.lerpDelta(entity.strength, 0f, 0.07f * Time.delta()); diff --git a/core/src/io/anuke/mindustry/world/blocks/units/UnitFactory.java b/core/src/io/anuke/mindustry/world/blocks/units/UnitFactory.java index ba30d25641..8eaaaeee70 100644 --- a/core/src/io/anuke/mindustry/world/blocks/units/UnitFactory.java +++ b/core/src/io/anuke/mindustry/world/blocks/units/UnitFactory.java @@ -148,7 +148,7 @@ public class UnitFactory extends Block{ if(hasRequirements(entity.items, entity.buildTime / produceTime) && entity.cons.valid()){ - entity.buildTime += entity.delta(); + entity.buildTime += entity.delta() * entity.power.satisfaction; entity.speedScl = Mathf.lerpDelta(entity.speedScl, 1f, 0.05f); }else{ entity.speedScl = Mathf.lerpDelta(entity.speedScl, 0f, 0.05f); diff --git a/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java b/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java index a38ece0027..18b9d57c40 100644 --- a/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java +++ b/core/src/io/anuke/mindustry/world/consumers/ConsumePower.java @@ -1,22 +1,53 @@ package io.anuke.mindustry.world.consumers; +import io.anuke.arc.math.Mathf; +import io.anuke.arc.scene.ui.layout.Table; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.meta.BlockStat; import io.anuke.mindustry.world.meta.BlockStats; import io.anuke.mindustry.world.meta.StatUnit; -import io.anuke.arc.scene.ui.layout.Table; +/** Consumer class for blocks which consume power while being connected to a power graph. */ public class ConsumePower extends Consume{ - protected final float use; + /** The maximum amount of power which can be processed per tick. This might influence efficiency or load a buffer. */ + protected final float powerPerTick; + /** The minimum power satisfaction (fraction of powerPerTick) which must be achieved before the module may work. */ + public final float minimumSatisfaction; + /** The maximum power capacity in power units. */ + public final float powerCapacity; + /** True if the module can store power. */ + public final boolean isBuffered; - public ConsumePower(float use){ - this.use = use; + protected ConsumePower(float powerPerTick, float minimumSatisfaction, float powerCapacity, boolean isBuffered){ + this.powerPerTick = powerPerTick; + this.minimumSatisfaction = minimumSatisfaction; + this.powerCapacity = powerCapacity; + this.isBuffered = isBuffered; + } + + /** + * Makes the owner consume powerPerTick each tick and disables it unless minimumSatisfaction (1.0 = 100%) of that power is being supplied. + * @param powerPerTick The maximum amount of power which is required per tick for 100% efficiency. + * @param minimumSatisfaction The percentage of powerPerTick which must be available for the module to work. + */ + public static ConsumePower consumePowerDirect(float powerPerTick, float minimumSatisfaction){ + return new ConsumePower(powerPerTick, minimumSatisfaction, 0.0f, false); + } + + /** + * Adds a power buffer to the owner which takes ticksToFill number of ticks to be filled. + * Note that this object does not remove power from the buffer. + * @param powerCapacity The maximum capacity in power units. + * @param ticksToFill The number of ticks it shall take to fill the buffer. + */ + public static ConsumePower consumePowerBuffered(float powerCapacity, float ticksToFill){ + return new ConsumePower(powerCapacity / ticksToFill, 0.0f, powerCapacity, true); } @Override public void buildTooltip(Table table){ - + // No tooltip for power } @Override @@ -26,21 +57,41 @@ public class ConsumePower extends Consume{ @Override public void update(Block block, TileEntity entity){ - if(entity.power == null) return; - entity.power.amount -= Math.min(use(block, entity), entity.power.amount); + // Nothing to do since PowerGraph directly updates entity.power.satisfaction } @Override public boolean valid(Block block, TileEntity entity){ - return entity.power != null && entity.power.amount >= use(block, entity); + if(isBuffered){ + return true; + }else{ + return entity.power.satisfaction >= minimumSatisfaction; + } } @Override public void display(BlockStats stats){ - stats.add(BlockStat.powerUse, use * 60f, StatUnit.powerSecond); + if(isBuffered){ + stats.add(BlockStat.powerCapacity, powerCapacity, StatUnit.powerSecond); + }else{ + stats.add(BlockStat.powerUse, powerPerTick * 60f, StatUnit.powerSecond); + } } - protected float use(Block block, TileEntity entity){ - return Math.min(use * entity.delta(), block.powerCapacity); + /** + * Retrieves the amount of power which is requested for the given block and entity. + * @param block The block which needs power. + * @param entity The entity which contains the power module. + * @return The amount of power which is requested per tick. + */ + public float requestedPower(Block block, TileEntity entity){ + if(isBuffered){ + // Stop requesting power once the buffer is full. + return Mathf.isEqual(entity.power.satisfaction, 1.0f) ? 0.0f : powerPerTick; + }else{ + return powerPerTick; + } } + + } diff --git a/core/src/io/anuke/mindustry/world/consumers/ConsumePowerExact.java b/core/src/io/anuke/mindustry/world/consumers/ConsumePowerExact.java deleted file mode 100644 index 8e9dfdd1b7..0000000000 --- a/core/src/io/anuke/mindustry/world/consumers/ConsumePowerExact.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.anuke.mindustry.world.consumers; - -import io.anuke.mindustry.entities.TileEntity; -import io.anuke.mindustry.world.Block; - -public class ConsumePowerExact extends ConsumePower{ - - public ConsumePowerExact(float use){ - super(use); - } - - @Override - protected float use(Block block, TileEntity entity){ - return this.use; - } -} diff --git a/core/src/io/anuke/mindustry/world/consumers/Consumers.java b/core/src/io/anuke/mindustry/world/consumers/Consumers.java index 27b475a9d6..f4ffaa27a8 100644 --- a/core/src/io/anuke/mindustry/world/consumers/Consumers.java +++ b/core/src/io/anuke/mindustry/world/consumers/Consumers.java @@ -30,18 +30,52 @@ public class Consumers{ } } - public ConsumePower power(float amount){ - ConsumePower p = new ConsumePower(amount); - add(p); - return p; - } - public ConsumeLiquid liquid(Liquid liquid, float amount){ ConsumeLiquid c = new ConsumeLiquid(liquid, amount); add(c); return c; } + /** + * Creates a consumer which directly uses power without buffering it. The module will work while at least 50% of power is supplied. + * @param powerPerTick The amount of power which is required each tick for 100% efficiency. + * @return the created consumer object. + */ + public ConsumePower power(float powerPerTick){ + return power(powerPerTick, 0.5f); + } + + /** + * Creates a consumer which directly uses power without buffering it. The module will work while the available power is greater than or equal to the minimumSatisfaction percentage (0..1). + * @param powerPerTick The amount of power which is required each tick for 100% efficiency. + * @return the created consumer object. + */ + public ConsumePower power(float powerPerTick, float minimumSatisfaction){ + ConsumePower c = ConsumePower.consumePowerDirect(powerPerTick, minimumSatisfaction); + add(c); + return c; + } + + /** + * Creates a consumer which stores power and uses it only in case of certain events (e.g. a turret firing). + * It will take 180 ticks (three second) to fill the buffer, given enough power supplied. + * @param powerCapacity The maximum capacity in power units. + */ + public ConsumePower powerBuffered(float powerCapacity){ + return powerBuffered(powerCapacity, 1f); + } + + /** + * Creates a consumer which stores power and uses it only in case of certain events (e.g. a turret firing). + * @param powerCapacity The maximum capacity in power units. + * @param ticksToFill The number of ticks it shall take to fill the buffer. + */ + public ConsumePower powerBuffered(float powerCapacity, float ticksToFill){ + ConsumePower c = ConsumePower.consumePowerBuffered(powerCapacity, ticksToFill); + add(c); + return c; + } + public ConsumeItem item(Item item){ return item(item, 1); } @@ -75,7 +109,7 @@ public class Consumers{ } public Consume add(Consume consume){ - map.put(consume.getClass(), consume); + map.put((consume instanceof ConsumePower ? ConsumePower.class : consume.getClass()), consume); return consume; } diff --git a/core/src/io/anuke/mindustry/world/modules/PowerModule.java b/core/src/io/anuke/mindustry/world/modules/PowerModule.java index 6cef939407..7e601394a1 100644 --- a/core/src/io/anuke/mindustry/world/modules/PowerModule.java +++ b/core/src/io/anuke/mindustry/world/modules/PowerModule.java @@ -8,14 +8,18 @@ import java.io.DataOutput; import java.io.IOException; public class PowerModule extends BlockModule{ - public float amount; + /** In case of unbuffered consumers, this is the percentage (1.0f = 100%) of the demanded power which can be supplied. + * Blocks will work at a reduced efficiency if this is not equal to 1.0f. + * In case of buffered consumers, this is the percentage of power stored in relation to the maximum capacity. + */ + public float satisfaction = 0.0f; + /** Specifies power which is required additionally, e.g. while a force projector is being shot at. */ + public float extraUse = 0f; public PowerGraph graph = new PowerGraph(); public IntArray links = new IntArray(); @Override public void write(DataOutput stream) throws IOException{ - stream.writeFloat(amount); - stream.writeShort(links.size); for(int i = 0; i < links.size; i++){ stream.writeInt(links.get(i)); @@ -24,15 +28,6 @@ public class PowerModule extends BlockModule{ @Override public void read(DataInput stream) throws IOException{ - amount = stream.readFloat(); - if(Float.isNaN(amount)){ - amount = 0f; - } - // Workaround: If power went negative for some reason, at least fix it when reloading the map - if(amount < 0f){ - amount = 0f; - } - short amount = stream.readShort(); for(int i = 0; i < amount; i++){ links.add(stream.readInt()); diff --git a/tests/src/test/java/power/DirectConsumerTests.java b/tests/src/test/java/power/DirectConsumerTests.java new file mode 100644 index 0000000000..e6081f7258 --- /dev/null +++ b/tests/src/test/java/power/DirectConsumerTests.java @@ -0,0 +1,55 @@ +package power; + +import io.anuke.mindustry.content.Items; +import io.anuke.mindustry.content.UnitTypes; +import io.anuke.mindustry.type.ItemStack; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; +import io.anuke.mindustry.world.blocks.power.PowerGraph; +import io.anuke.mindustry.world.blocks.units.UnitFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** Tests for direct power consumers. */ +public class DirectConsumerTests extends PowerTestFixture{ + + @Test + void noPowerRequestedWithNoItems(){ + testUnitFactory(0, 0, 0.08f, 0.08f, 0.0f); + } + + @Test + void noPowerRequestedWithInsufficientItems(){ + testUnitFactory(30, 0, 0.08f, 0.08f, 0.0f); + testUnitFactory(0, 30, 0.08f, 0.08f, 0.0f); + } + + @Test + void powerRequestedWithSufficientItems(){ + testUnitFactory(30, 30, 0.08f, 0.08f, 1.0f); + } + + void testUnitFactory(int siliconAmount, int leadAmount, float producedPower, float requestedPower, float expectedSatisfaction){ + Tile consumerTile = createFakeTile(0, 0, new UnitFactory("fakefactory"){{ + type = UnitTypes.spirit; + produceTime = 60; + consumes.power(requestedPower); + consumes.items(new ItemStack(Items.silicon, 30), new ItemStack(Items.lead, 30)); + }}); + consumerTile.entity.items.add(Items.silicon, siliconAmount); + consumerTile.entity.items.add(Items.lead, leadAmount); + + Tile producerTile = createFakeTile(2, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 0.5f; // 100% + + PowerGraph graph = new PowerGraph(); + graph.add(producerTile); + graph.add(consumerTile); + + consumerTile.entity.update(); + graph.update(); + + assertEquals(expectedSatisfaction, consumerTile.entity.power.satisfaction); + } +} diff --git a/tests/src/test/java/power/ItemLiquidGeneratorTests.java b/tests/src/test/java/power/ItemLiquidGeneratorTests.java new file mode 100644 index 0000000000..83f58a3e49 --- /dev/null +++ b/tests/src/test/java/power/ItemLiquidGeneratorTests.java @@ -0,0 +1,175 @@ +package power; + +import io.anuke.arc.util.Time; +import io.anuke.mindustry.content.Items; +import io.anuke.mindustry.content.Liquids; +import io.anuke.mindustry.type.Item; +import io.anuke.mindustry.type.Liquid; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.power.ItemLiquidGenerator; +import org.junit.jupiter.api.*; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +/** + * This class tests generators which can process items, liquids or both. + * All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well. + * Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame. + * Both of these constraints are handled by FakeThreadHandler within PowerTestFixture. + * Any expected power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not! + */ +public class ItemLiquidGeneratorTests extends PowerTestFixture{ + + private ItemLiquidGenerator generator; + private Tile tile; + private ItemLiquidGenerator.ItemLiquidGeneratorEntity entity; + private final float fakeItemDuration = 60f; // 60 ticks + private final float maximumLiquidUsage = 0.5f; + + public void createGenerator(ItemLiquidGenerator.InputType inputType){ + generator = new ItemLiquidGenerator(inputType, "fakegen"){ + { + powerProduction = 0.1f; + itemDuration = 60f; + itemDuration = fakeItemDuration; + maxLiquidGenerate = maximumLiquidUsage; + } + + @Override + public float getItemEfficiency(Item item){ + return item.flammability; + } + + @Override + public float getLiquidEfficiency(Liquid liquid){ + return liquid.flammability; + } + }; + + tile = createFakeTile(0, 0, generator); + entity = tile.entity(); + } + + /** Tests the consumption and efficiency when being supplied with liquids. */ + @TestFactory + DynamicTest[] generatorWorksProperlyWithLiquidInput(){ + + // Execute all tests for the case where only liquids are accepted and for the case where liquids and items are accepted (but supply only liquids) + ItemLiquidGenerator.InputType[] inputTypesToBeTested = new ItemLiquidGenerator.InputType[]{ + ItemLiquidGenerator.InputType.LiquidsOnly, + ItemLiquidGenerator.InputType.LiquidsAndItems + }; + + ArrayList tests = new ArrayList<>(); + for(ItemLiquidGenerator.InputType inputType : inputTypesToBeTested){ + tests.add(dynamicTest("01", () -> simulateLiquidConsumption(inputType, Liquids.oil, 0.0f, "No liquids provided"))); + tests.add(dynamicTest("02", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage / 4.0f, "Low oil provided"))); + tests.add(dynamicTest("03", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage * 1.0f, "Sufficient oil provided"))); + tests.add(dynamicTest("04", () -> simulateLiquidConsumption(inputType, Liquids.oil, maximumLiquidUsage * 2.0f, "Excess oil provided"))); + // Note: The generator will decline any other liquid since it's not flammable + } + DynamicTest[] testArray = new DynamicTest[tests.size()]; + testArray = tests.toArray(testArray); + return testArray; + } + + void simulateLiquidConsumption(ItemLiquidGenerator.InputType inputType, Liquid liquid, float availableLiquidAmount, String parameterDescription){ + final float baseEfficiency = liquid.flammability; + final float expectedEfficiency = Math.min(1.0f, availableLiquidAmount / maximumLiquidUsage) * baseEfficiency; + final float expectedConsumptionPerTick = Math.min(maximumLiquidUsage, availableLiquidAmount); + final float expectedRemainingLiquidAmount = Math.max(0.0f, availableLiquidAmount - expectedConsumptionPerTick * Time.delta()); + + createGenerator(inputType); + assertTrue(generator.acceptLiquid(tile, null, liquid, availableLiquidAmount), inputType + " | " + parameterDescription + ": Liquids which will be declined by the generator don't need to be tested - The code won't be called for those cases."); + + entity.liquids.add(liquid, availableLiquidAmount); + entity.cons.update(tile.entity); + assertTrue(entity.cons.valid()); + + // Perform an update on the generator once - This should use up any resource up to the maximum liquid usage + generator.update(tile); + + assertEquals(expectedRemainingLiquidAmount, entity.liquids.get(liquid), inputType + " | " + parameterDescription + ": Remaining liquid amount mismatch."); + assertEquals(expectedEfficiency, entity.productionEfficiency, inputType + " | " + parameterDescription + ": Efficiency mismatch."); + } + + /** Tests the consumption and efficiency when being supplied with items. */ + @TestFactory + DynamicTest[] generatorWorksProperlyWithItemInput(){ + + // Execute all tests for the case where only items are accepted and for the case where liquids and items are accepted (but supply only items) + ItemLiquidGenerator.InputType[] inputTypesToBeTested = new ItemLiquidGenerator.InputType[]{ + ItemLiquidGenerator.InputType.ItemsOnly, + ItemLiquidGenerator.InputType.LiquidsAndItems + }; + + ArrayList tests = new ArrayList<>(); + for(ItemLiquidGenerator.InputType inputType : inputTypesToBeTested){ + tests.add(dynamicTest("01", () -> simulateItemConsumption(inputType, Items.coal, 0, "No items provided"))); + tests.add(dynamicTest("02", () -> simulateItemConsumption(inputType, Items.coal, 1, "Sufficient coal provided"))); + tests.add(dynamicTest("03", () -> simulateItemConsumption(inputType, Items.coal, 10, "Excess coal provided"))); + tests.add(dynamicTest("04", () -> simulateItemConsumption(inputType, Items.blastCompound, 1, "Blast compound provided"))); + //dynamicTest("03", () -> simulateItemConsumption(inputType, Items.plastanium, 1, "Plastanium provided")), // Not accepted by generator due to low flammability + tests.add(dynamicTest("05", () -> simulateItemConsumption(inputType, Items.biomatter, 1, "Biomatter provided"))); + tests.add(dynamicTest("06", () -> simulateItemConsumption(inputType, Items.pyratite, 1, "Pyratite provided"))); + } + DynamicTest[] testArray = new DynamicTest[tests.size()]; + testArray = tests.toArray(testArray); + return testArray; + } + + void simulateItemConsumption(ItemLiquidGenerator.InputType inputType, Item item, int amount, String parameterDescription){ + final float expectedEfficiency = Math.min(1.0f, amount > 0 ? item.flammability : 0f); + final float expectedRemainingItemAmount = Math.max(0, amount - 1); + + createGenerator(inputType); + assertTrue(generator.acceptItem(item, tile, null), inputType + " | " + parameterDescription + ": Items which will be declined by the generator don't need to be tested - The code won't be called for those cases."); + + if(amount > 0){ + entity.items.add(item, amount); + } + entity.cons.update(tile.entity); + assertTrue(entity.cons.valid()); + + // Perform an update on the generator once - This should use up one or zero items - dependent on if the item is accepted and available or not. + generator.update(tile); + + assertEquals(expectedRemainingItemAmount, entity.items.get(item), inputType + " | " + parameterDescription + ": Remaining item amount mismatch."); + assertEquals(expectedEfficiency, entity.productionEfficiency, inputType + " | " + parameterDescription + ": Efficiency mismatch."); + } + + /** Makes sure the efficiency stays equal during the item duration. */ + @Test + void efficiencyRemainsConstantWithinItemDuration_ItemsOnly(){ + testItemDuration(ItemLiquidGenerator.InputType.ItemsOnly); + } + + /** Makes sure the efficiency stays equal during the item duration. */ + @Test + void efficiencyRemainsConstantWithinItemDuration_ItemsAndLiquids(){ + testItemDuration(ItemLiquidGenerator.InputType.LiquidsAndItems); + } + + void testItemDuration(ItemLiquidGenerator.InputType inputType){ + createGenerator(inputType); + + // Burn a single coal and test for the duration + entity.items.add(Items.coal, 1); + entity.cons.update(tile.entity); + generator.update(tile); + + float expectedEfficiency = entity.productionEfficiency; + + float currentDuration = 0.0f; + while((currentDuration += Time.delta()) <= fakeItemDuration){ + generator.update(tile); + assertEquals(expectedEfficiency, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration)); + } + generator.update(tile); + assertEquals(0.0f, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration)); + } +} diff --git a/tests/src/test/java/power/PowerTestFixture.java b/tests/src/test/java/power/PowerTestFixture.java new file mode 100644 index 0000000000..3c4e6e9327 --- /dev/null +++ b/tests/src/test/java/power/PowerTestFixture.java @@ -0,0 +1,105 @@ +package power; + +import io.anuke.arc.math.Mathf; +import io.anuke.arc.util.Time; +import io.anuke.mindustry.Vars; +import io.anuke.mindustry.content.blocks.Blocks; +import io.anuke.mindustry.core.ContentLoader; +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.PowerBlock; +import io.anuke.mindustry.world.blocks.power.Battery; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; +import io.anuke.mindustry.world.modules.ConsumeModule; +import io.anuke.mindustry.world.modules.ItemModule; +import io.anuke.mindustry.world.modules.LiquidModule; +import io.anuke.mindustry.world.modules.PowerModule; +import org.junit.jupiter.api.BeforeAll; + +import java.lang.reflect.Field; + +/** This class provides objects commonly used by power related unit tests. + * For now, this is a helper with static methods, but this might change. + * + * Note: All tests which subclass this will run with a fixed delta of 0.5! + * */ +public class PowerTestFixture{ + + public static final float smallRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR; + public static final float mediumRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR * 10; + public static final float highRoundingTolerance = Mathf.FLOAT_ROUNDING_ERROR * 100; + + @BeforeAll + static void initializeDependencies(){ + Vars.content = new ContentLoader(); + Vars.content.load(); + Time.setDeltaProvider(() -> 0.5f); + } + + protected static PowerGenerator createFakeProducerBlock(float producedPower){ + // Multiply produced power by 2 since production efficiency is defined to be 0.5 = 100% + return new PowerGenerator("fakegen"){{ + powerProduction = producedPower * 2.0f; + }}; + } + + protected static Battery createFakeBattery(float capacity, float ticksToFill){ + return new Battery("fakebattery"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + + protected static Block createFakeDirectConsumer(float powerPerTick, float minimumSatisfaction){ + return new PowerBlock("fakedirectconsumer"){{ + consumes.power(powerPerTick, minimumSatisfaction); + }}; + } + + protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){ + return new PowerBlock("fakebufferedconsumer"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + /** + * Creates a fake tile on the given location using the given block. + * @param x The X coordinate. + * @param y The y coordinate. + * @param block The block on the tile. + * @return The created tile or null in case of exceptions. + */ + protected static Tile createFakeTile(int x, int y, Block block){ + try{ + Tile tile = new Tile(x, y); + + // Using the Tile(int, int, byte, byte) constructor would require us to register any fake block or tile we create + // Since this part shall not be part of the test and would require more work anyway, we manually set the block and floor + // through reflections and then simulate part of what the changed() method does. + + Field field = Tile.class.getDeclaredField("wall"); + field.setAccessible(true); + field.set(tile, block); + + field = Tile.class.getDeclaredField("floor"); + field.setAccessible(true); + field.set(tile, Blocks.sand); + + // Simulate the "changed" method. Calling it through reflections would require half the game to be initialized. + tile.entity = block.newEntity().init(tile, false); + tile.entity.cons = new ConsumeModule(); + if(block.hasItems) tile.entity.items = new ItemModule(); + if(block.hasLiquids) tile.entity.liquids = new LiquidModule(); + if(block.hasPower){ + tile.entity.power = new PowerModule(); + tile.entity.power.graph.add(tile); + } + + // Assign incredibly high health so the block does not get destroyed on e.g. burning Blast Compound + block.health = 100000; + tile.entity.health = 100000.0f; + + return tile; + }catch(Exception ex){ + return null; + } + } +} diff --git a/tests/src/test/java/power/PowerTests.java b/tests/src/test/java/power/PowerTests.java new file mode 100644 index 0000000000..db37e3668a --- /dev/null +++ b/tests/src/test/java/power/PowerTests.java @@ -0,0 +1,183 @@ +package power; + +import io.anuke.arc.math.Mathf; +import io.anuke.arc.util.Time; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; +import io.anuke.mindustry.world.blocks.power.PowerGraph; +import io.anuke.mindustry.world.consumers.ConsumePower; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +/** + * Tests code related to the power system in general, but not specific blocks. + * All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well. + * Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame. + * Both of these constraints are handled by FakeThreadHandler within PowerTestFixture. + * Any power amount (produced, consumed, buffered) should be affected by Time.delta() but satisfaction should not! + */ +public class PowerTests extends PowerTestFixture{ + + @BeforeEach + void initTest(){ + } + + @Nested + class PowerGraphTests{ + + /** Tests the satisfaction of a single consumer after a single update of the power graph which contains a single producer. + * + * Assumption: When the consumer requests zero power, satisfaction does not change. Default is 0.0f. + */ + @TestFactory + DynamicTest[] directConsumerSatisfactionIsAsExpected(){ + return new DynamicTest[]{ + // Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975 + // That's why we inject the description into the test method for now. + // Additional Note: If you don't see any labels in front of the values supplied as function parameters, use a better IDE like IntelliJ IDEA. + dynamicTest("01", () -> simulateDirectConsumption(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")), + dynamicTest("02", () -> simulateDirectConsumption(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")), + dynamicTest("03", () -> simulateDirectConsumption(1.0f, 0.0f, 0.0f, "1.0 produced, 0.0 consumed (no power requested)")), + dynamicTest("04", () -> simulateDirectConsumption(1.0f, 1.0f, 1.0f, "1.0 produced, 1.0 consumed (stable consumption)")), + dynamicTest("05", () -> simulateDirectConsumption(0.5f, 1.0f, 0.5f, "0.5 produced, 1.0 consumed (power shortage)")), + dynamicTest("06", () -> simulateDirectConsumption(1.0f, 0.5f, 1.0f, "1.0 produced, 0.5 consumed (power excess)")), + dynamicTest("07", () -> simulateDirectConsumption(0.09f, 0.09f - Mathf.FLOAT_ROUNDING_ERROR / 10.0f, 1.0f, "floating point inaccuracy (stable consumption)")) + }; + } + void simulateDirectConsumption(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 0.5f; // Currently, 0.5f = 100% + Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower, 0.6f)); + + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(directConsumerTile); + + assertEquals(producedPower * Time.delta(), powerGraph.getPowerProduced(), Mathf.FLOAT_ROUNDING_ERROR); + assertEquals(requiredPower * Time.delta(), powerGraph.getPowerNeeded(), Mathf.FLOAT_ROUNDING_ERROR); + + // Update and check for the expected power satisfaction of the consumer + powerGraph.update(); + assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match"); + } + + /** Tests the satisfaction of a single buffered consumer after a single update of the power graph which contains a single producer. */ + @TestFactory + DynamicTest[] bufferedConsumerSatisfactionIsAsExpected(){ + return new DynamicTest[]{ + // Note: powerPerTick may not be 0 in any of the test cases. This would equal a "ticksToFill" of infinite. + // Note: Due to a fixed delta of 0.5, only half of what is defined here will in fact be produced/consumed. Keep this in mind when defining expectedSatisfaction! + dynamicTest("01", () -> simulateBufferedConsumption(0.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power anywhere")), + dynamicTest("02", () -> simulateBufferedConsumption(0.0f, 1.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power provided")), + dynamicTest("03", () -> simulateBufferedConsumption(1.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power requested")), + dynamicTest("04", () -> simulateBufferedConsumption(1.0f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Stable Power, One tick to fill")), + dynamicTest("05", () -> simulateBufferedConsumption(2.0f, 1.0f, 2.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One delta to fill")), + dynamicTest("06", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Stable Power, multiple ticks to fill")), + dynamicTest("07", () -> simulateBufferedConsumption(1.2f, 0.5f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Power excess, one delta to fill")), + dynamicTest("08", () -> simulateBufferedConsumption(1.0f, 0.5f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power excess, multiple ticks to fill")), + dynamicTest("09", () -> simulateBufferedConsumption(1.0f, 1.0f, 2.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one delta to fill")), + dynamicTest("10", () -> simulateBufferedConsumption(0.5f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Power shortage, multiple ticks to fill")), + dynamicTest("11", () -> simulateBufferedConsumption(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")), + dynamicTest("12", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")), + dynamicTest("13", () -> simulateBufferedConsumption(0.2f, 1.0f, 0.5f, 0.5f, 0.6f, "Half buffer, power shortage")), + dynamicTest("14", () -> simulateBufferedConsumption(1.0f, 1.0f, 0.5f, 0.9f, 1.0f, "Buffer does not get exceeded")), + dynamicTest("15", () -> simulateBufferedConsumption(2.0f, 1.0f, 1.0f, 0.5f, 1.0f, "Half buffer, filled with excess")) + }; + } + void simulateBufferedConsumption(float producedPower, float maxBuffer, float powerConsumedPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 0.5f; // Currently, 0.5 = 100% + Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerConsumedPerTick : 1.0f)); + bufferedConsumerTile.entity.power.satisfaction = initialSatisfaction; + + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(bufferedConsumerTile); + + assertEquals(producedPower * Time.delta(), powerGraph.getPowerProduced(), Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Produced power did not match"); + float expectedPowerUsage; + if(initialSatisfaction == 1.0f){ + expectedPowerUsage = 0f; + }else{ + expectedPowerUsage = Math.min(maxBuffer, powerConsumedPerTick * Time.delta()); + } + assertEquals(expectedPowerUsage, powerGraph.getPowerNeeded(), Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Consumed power did not match"); + + // Update and check for the expected power satisfaction of the consumer + powerGraph.update(); + assertEquals(expectedSatisfaction, bufferedConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of buffered consumer did not match"); + } + + /** Tests the satisfaction of a single direct consumer after a single update of the power graph which contains a single producer and a single battery. + * The used battery is created with a maximum capacity of 100 and receives ten power per tick. + */ + @TestFactory + DynamicTest[] batteryCapacityIsAsExpected(){ + return new DynamicTest[]{ + // Note: expectedBatteryCapacity is currently adjusted to a delta of 0.5! (FakeThreadHandler sets it to that) + dynamicTest("01", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 0.0f, 5.0f, 0.0f, "Empty battery, no consumer")), + dynamicTest("02", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 94.999f, 99.999f, 0.0f, "Battery almost full after update, no consumer")), + dynamicTest("03", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 100.0f, 100.0f, 0.0f, "Full battery, no consumer")), + dynamicTest("04", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, "No producer, no consumer, empty battery")), + dynamicTest("05", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 100.0f, 100.0f, 0.0f, "No producer, no consumer, full battery")), + dynamicTest("06", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 0.0f, 0.0f, 0.0f, "No producer, empty battery")), + dynamicTest("07", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 100.0f, 95.0f, 1.0f, "No producer, full battery")), + dynamicTest("08", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 2.5f, 0.0f, 0.5f, "No producer, low battery")), + dynamicTest("09", () -> simulateDirectConsumptionWithBattery(5.0f, 10.0f, 5.0f, 0.0f, 1.0f, "Producer + Battery = Consumed")), + }; + } + void simulateDirectConsumptionWithBattery(float producedPower, float requestedPower, float initialBatteryCapacity, float expectedBatteryCapacity, float expectedSatisfaction, String parameterDescription){ + PowerGraph powerGraph = new PowerGraph(); + + if(producedPower > 0.0f){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + producerTile.entity().productionEfficiency = 0.5f; + powerGraph.add(producerTile); + } + Tile directConsumerTile = null; + if(requestedPower > 0.0f){ + directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requestedPower, 0.6f)); + powerGraph.add(directConsumerTile); + } + float maxCapacity = 100f; + Tile batteryTile = createFakeTile(0, 2, createFakeBattery(maxCapacity, 10 )); + batteryTile.entity.power.satisfaction = initialBatteryCapacity / maxCapacity; + + powerGraph.add(batteryTile); + + powerGraph.update(); + assertEquals(expectedBatteryCapacity / maxCapacity, batteryTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery satisfaction did not match"); + if(directConsumerTile != null){ + assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match"); + } + } + + /** Makes sure a direct consumer stops working after power production is set to zero. */ + @Test + void directConsumptionStopsWithNoPower(){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(10.0f)); + producerTile.entity().productionEfficiency = 1.0f; + Tile consumerTile = createFakeTile(0, 1, createFakeDirectConsumer(5.0f, 0.6f)); + + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(consumerTile); + powerGraph.update(); + + assertEquals(1.0f, consumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR); + + powerGraph.remove(producerTile); + powerGraph.add(consumerTile); + powerGraph.update(); + + assertEquals(0.0f, consumerTile.entity.power.satisfaction, Mathf.FLOAT_ROUNDING_ERROR); + if(consumerTile.block().consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumerTile.block().consumes.get(ConsumePower.class); + assertFalse(consumePower.valid(consumerTile.block(), consumerTile.entity())); + } + } + } +}