1 Commits

462 changed files with 2289 additions and 12479 deletions
+3 -9
View File
@@ -1,19 +1,13 @@
---
name: Godot 4.7 Core Rules
name: Godot 4.4 Core Rules
priority: high
---
Ты — senior Godot 4.7 GDScript разработчик.
- Use Godot 4.7+ API.
- Do not use Godot 3.x syntax or APIs.
- Follow the existing project architecture and coding style.
- Do not introduce unnecessary abstractions.
- Do not modify unrelated files.
Ты — senior Godot 4.4 GDScript разработчик.
- Всегда используй Godot 4.4+ API.
- Пиши строго типизированный код (`var health: int = 100`, `: Array[Node]` и т.д.).
- Предпочитай Signals вместо проверки в _process.
- Используй @export, @export_range, @export_enum, @onready.
- Composition over deep inheritance.
- Менеджеры и автолоады выноси в отдельные singleton'ы.
-1
View File
@@ -1 +0,0 @@
- use spaces not tabs for indentation
-107
View File
@@ -1,107 +0,0 @@
name: Android production release
on:
push:
tags:
- "v*"
jobs:
test-build-publish:
runs-on: godot
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Verify tag belongs to main
run: |
git fetch origin main
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
- name: Show Godot version
run: godot --version
- name: Import project
run: |
rm -rf .godot
godot --headless --path . --editor --import --quit --verbose
- name: Run all tests
shell: bash
run: |
set -euo pipefail
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
test "${#tests[@]}" -gt 0
for test_file in "${tests[@]}"; do
echo "::group::${test_file}"
godot --headless --path . --script "res://${test_file}"
echo "::endgroup::"
done
- name: Prepare Android signing
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
test -n "${ANDROID_KEYSTORE_BASE64}"
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${RUNNER_TEMP}/release.keystore"
test -s "${RUNNER_TEMP}/release.keystore"
- name: Configure release AAB
env:
RELEASE_VERSION: ${{ github.ref_name }}
RELEASE_VERSION_CODE: ${{ github.run_number }}
run: |
python3 - <<'PY'
import os
import re
from pathlib import Path
path = Path("export_presets.cfg")
text = path.read_text()
start = text.index('[preset.1.options]')
head, android = text[:start], text[start:]
replacements = (
(r'gradle_build/use_gradle_build=false', 'gradle_build/use_gradle_build=true'),
(r'gradle_build/export_format=0', 'gradle_build/export_format=1'),
(r'package/signed=false', 'package/signed=true'),
(r'version/code=\d+', f'version/code={100000 + int(os.environ["RELEASE_VERSION_CODE"])}'),
(r'version/name="[^"]*"', f'version/name="{os.environ["RELEASE_VERSION"].removeprefix("v")}"'),
)
for pattern, replacement in replacements:
android, count = re.subn(pattern, replacement, android, count=1)
if count != 1:
raise RuntimeError(f'Expected one Android preset match for {pattern!r}, got {count}')
path.write_text(head + android)
PY
- name: Build signed Android App Bundle
env:
GODOT_ANDROID_KEYSTORE_RELEASE_USER: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
run: |
export GODOT_ANDROID_KEYSTORE_RELEASE_PATH="${RUNNER_TEMP}/release.keystore"
mkdir -p build
godot --headless --path . --export-release "Android" build/owniverse-${GITHUB_REF_NAME}.aab
test -s "build/owniverse-${GITHUB_REF_NAME}.aab"
ls -lh "build/owniverse-${GITHUB_REF_NAME}.aab"
- name: Publish to Google Play production
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.KIDGameProduction.Owniverse
releaseFiles: build/owniverse-${{ github.ref_name }}.aab
tracks: production
status: completed
- name: Upload release bundle
uses: actions/upload-artifact@v3
with:
name: owniverse-${{ github.ref_name }}-aab
path: build/owniverse-${{ github.ref_name }}.aab
if-no-files-found: error
retention-days: 90
+55
View File
@@ -0,0 +1,55 @@
name: Android
on:
push:
branches:
- main
jobs:
android:
runs-on: godot
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Godot version
run: |
godot --version
- name: Java version
run: |
java -version
- name: Android SDK
run: |
adb version
- name: Build APK
run: |
mkdir -p build
godot \
--headless \
--path . \
--export-release "Android" \
build/app.apk
- name: Verify APK
run: |
test -f build/app.apk
ls -lh build/app.apk
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: android-apk
path: build/app.apk
-62
View File
@@ -1,62 +0,0 @@
name: Testing builds
on:
push:
branches:
- testing
jobs:
test-and-build:
runs-on: godot
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Show Godot version
run: godot --version
- name: Import project
run: |
rm -rf .godot
godot --headless --path . --editor --import --quit --verbose
- name: Run all tests
shell: bash
run: |
set -euo pipefail
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
test "${#tests[@]}" -gt 0
for test_file in "${tests[@]}"; do
echo "::group::${test_file}"
godot --headless --path . --script "res://${test_file}"
echo "::endgroup::"
done
- name: Build Android APK
run: |
mkdir -p build
godot --headless --path . --export-release "Android" build/owniverse-testing.apk
- name: Build Windows EXE
run: |
godot --headless --path . --export-release "Windows Desktop" build/owniverse-testing.exe
- name: Verify builds
run: |
test -s build/owniverse-testing.apk
test -s build/owniverse-testing.exe
ls -lh build/owniverse-testing.apk build/owniverse-testing.exe
- name: Upload testing builds
uses: actions/upload-artifact@v3
with:
name: owniverse-testing-${{ github.sha }}
path: |
build/owniverse-testing.apk
build/owniverse-testing.exe
if-no-files-found: error
retention-days: 14
-1
View File
@@ -3,7 +3,6 @@
.vscode/
/android/
*.import
!data/*.txt.import
# MacOS specific ignores
.DS_Store
+19 -49
View File
@@ -1,10 +1,6 @@
extends Node
const E = preload("res://world/owniverse/math/exact.gd")
const SPEED_LIMIT: int = 6
# Write a readable diagnostic copy next to the encrypted save. Never used for loading.
var save_plaintext_copy: bool = true
const SPEED_LIMIT: int = 4
var Settings: Dictionary = {
"filename": "save-0001",
@@ -20,8 +16,6 @@ var Locale = "en"
var OwniverseEntities: Dictionary = {}
var OwniverseItems: Dictionary = {}
var Templates: Dictionary = {}
var LifeRules: Dictionary = {}
var CivilizationRules: Dictionary = {}
var batch_count: float = 40.0 # must be even not odd
@@ -32,59 +26,35 @@ func get_localized_item_description(item_id: String, item: String) -> String:
return ""
# Russian templates include the class; legacy English keys omit it.
func get_template(id: String, category: String, field: String) -> Dictionary:
var rows: Dictionary = Templates.get(Locale, {})
for key in [id + "." + category + "." + field, id + "." + field]:
if rows.has(key):
return rows[key]
return {}
func render_template(row: Dictionary, variables: Dictionary) -> String:
var result: String = str(row.get("text", ""))
for i in range(5):
var variable: String = str(row.get("value" + str(i), "")).strip_edges()
var value: String = str(variables.get(variable, "—")) if variable != "" else ""
result = result.replace("%value" + str(i) + "%", value)
return result.replace("<br>", "\n").replace("[p]", "\n")
func get_description(item_id: String, items: Array = []) -> String:
var row: Dictionary = Templates.get(Locale, {}).get(item_id, {})
var variables: Dictionary = {}
for i in mini(items.size(), 5):
variables[str(row.get("value" + str(i), ""))] = items[i]
return render_template(row, variables)
if !Templates[Locale].has(item_id):
return ""
var description: String = Templates[Locale][item_id]["text"]
var value = 0
for item in items:
description.replace("value" + str(value), str(item))
return description
func format_number(value, digits: int = 0) -> String:
return E.grouped(value, digits)
func format_number(value: float, digits: int = 0) -> String:
if value == 0: return ""
if value < 10_000:
return "%.*f" % [digits, value]
var exp_value = int(floor(log(abs(value)) / log(10.0)))
var mantissa = value / pow(10.0, exp_value)
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
func _ready() -> void:
CivilizationRules.clear()
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
if not CivilizationRules.has(row.stage):
CivilizationRules[row.stage] = {"home": row.home, "span": E.of(row.span), "stars": E.of(row.stars), "ethics": {},
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
CivilizationRules[row.stage].ethics[row.ethic] = row
var stages: Array = CivilizationRules.keys()
stages.sort()
for i in stages.size():
CivilizationRules[stages[i]].next = stages[i + 1] if i + 1 < stages.size() else "Ct"
CivilizationRules[stages[i]].previous = stages[i - 1] if i > 0 else "C0.1"
CivilizationRules[stages[i]].colony = stages[maxi(0, i - 2)]
CivilizationRules.Ct = {"home": "", "span": 0.0, "stars": 0.0, "ethics": {}, "next": "Ct", "previous": "C4.4", "colonizes": false, "fights": false, "diplomacy": false, "worships": false}
LifeRules = _read_from_csv("res://data/life_rules.txt")
OwniverseEntities = _read_from_csv("res://data/owniverse_entities.txt")
OwniverseEntities = _read_from_csv("res://data/owniverse_entitites.txt")
OwniverseItems = _read_from_csv("res://data/owniverse_items.txt")
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
Templates = OwniverseTemplate.get_data()
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
func _read_from_csv(file_path: String) -> Dictionary:
+1
View File
@@ -0,0 +1 @@
uid://dojk16crca3cx
+56
View File
@@ -0,0 +1,56 @@
# SaveManager.gd (добавить в AutoLoad)
extends Node
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
const SETTING_SECTION := "Settings"
const SAVE_PATH = "user://settings.cfg"
#region Save Load routines
func save_data(filename: String, password: String, data: Dictionary) -> void:
filename = "user://" + filename + ".adventure"
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.WRITE, password)
if file == null:
push_error("Can't write to the file: ", filename)
return
file.store_var(data) # бинарно + быстрее + сериализация
file.close()
func load_data(filename: String, password: String) -> Dictionary:
filename = "user://" + filename + ".adventure"
if not FileAccess.file_exists(filename):
return {}
var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.READ, password)
if file == null:
push_error(filename, " couldn't be decrypted.")
return {}
var data: Variant = file.get_var()
file.close()
return data if data is Dictionary else {}
#endregion
#region Settings
func save_settings():
var config = ConfigFile.new()
for item in Global.Settings:
config.set_value(SETTING_SECTION, item, Global.Settings[item])
config.save(SAVE_PATH)
func load_settings():
var config = ConfigFile.new()
var err = config.load(SAVE_PATH)
if err != OK:
return false
for item in Global.Settings:
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
return true
#endregion
+306
View File
@@ -0,0 +1,306 @@
extends Control
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const PAGE_COUNT = 5
const PAGE_WIDTH = 1080
const DRAG_TOLERANCE = 360
const SWIPE_DURATION: float = 0.4
@onready var stage_page_scroller: ScrollContainer = $StageRows/PageScroller
@onready var item_pages: Array[Control] = [
$StageRows/PageScroller/HBoxContainer/ForcePage/Forces,
$StageRows/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
$StageRows/PageScroller/HBoxContainer/StarPage/Stars,
$StageRows/PageScroller/HBoxContainer/StructurePage/Structures,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Life,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ4
]
@onready var page_actions: Dictionary[int, OwniverseAction] = {
2: $StageRows/PageScroller/HBoxContainer/StarPage/Actions,
3: $StageRows/PageScroller/HBoxContainer/StructurePage/Actions,
}
@onready var g_item_descriptions: Array = [
$StageRows/PageScroller/HBoxContainer/ForcePage/Description,
$StageRows/PageScroller/HBoxContainer/ResourcePage/Description,
$StageRows/PageScroller/HBoxContainer/StarPage/Description,
$StageRows/PageScroller/HBoxContainer/StructurePage/Description,
$StageRows/PageScroller/HBoxContainer/LifePage/Description
]
@onready var date_time: Label = $StageRows/TopMenu/InfoPanel/DateTime
@onready var g_speed_icons: Array = [
$StageRows/TopMenu/InfoPanel/Speed/Speed1,
$StageRows/TopMenu/InfoPanel/Speed/Speed2,
$StageRows/TopMenu/InfoPanel/Speed/Speed3,
$StageRows/TopMenu/InfoPanel/Speed/Speed4,
$StageRows/TopMenu/InfoPanel/Speed/Speed5,
$StageRows/TopMenu/InfoPanel/Speed/Speed6,
$StageRows/TopMenu/InfoPanel/Speed/Speed7,
$StageRows/TopMenu/InfoPanel/Speed/Speed8
]
@onready var g_time_minus: TextureButton = $StageRows/TopMenu/TimeMinus
@onready var g_time_plus: TextureButton = $StageRows/TopMenu/TimePlus
var g_tween: Tween = null
var g_dragged_start: Vector2 = Vector2.ZERO
var g_scroll_start: int = 0
#var g_current_page: int = 0
var g_last_scroll_updated: int = 0
var f_check_swipe: bool = false
var g_swipe_time_delta: float = 0
var g_counters: Dictionary[String, Counter] = {
"swipe": Counter.new(),
}
var g_active_items: Dictionary = {}
var owniverse:Owniverse = Owniverse.new()
var owniverse_items: Dictionary[String, OwniverseItem] = {}
var item_list_to_update: Dictionary = {}
func set_datetime(value: float):
date_time.text = str(snapped(value, 0.1))
#region Item manipulations: update and visibility
func add_to_update_list(item_id: String, amount: int) -> void:
if item_list_to_update.has(item_id):
item_list_to_update[item_id] += amount
else:
item_list_to_update[item_id] = amount
func set_owniverse_item_value() -> void:
for item_id in owniverse.items_to_update:
if owniverse_items.has(item_id):
owniverse_items[item_id].set_label(owniverse.items_to_update[item_id])
owniverse.items_to_update = {}
#endregion
func _on_indicator_pressed(indicator_name: String, page: int = 0):
_set_description(indicator_name, page)
func _set_item_description(item: OwniverseItem) -> void:
var description: String = "[b]" \
+ Global.get_localized_item_description(item.item_name, "label") \
+"[/b]" \
+"\n" \
+ Global.get_localized_item_description(item.item_name, "description")
var desc_page = clamp(item.item_page, 0, 4)
g_item_descriptions[desc_page].text = description
func _set_description(item_name: String, page: int) -> void:
var description = Global.get_description(item_name + ".Name")
description += "\n"
description += Global.get_description(item_name + ".Description")
g_item_descriptions[page].text = description
func _control_page(item: OwniverseItem) -> void:
# TODO переписать это ↓
if !g_active_items.has(item.item_page):
g_active_items[item.item_page] = null
var current_item: OwniverseItem = g_active_items[item.item_page]
if current_item == item:
return
g_active_items[item.item_page] = item
if current_item != null and current_item.item_page == item.item_page:
current_item.set_selection(false)
item.set_selection(true)
# subpages
if item.item_sub_page != -1:
item_pages[item.item_sub_page].visible = true
if current_item == null \
or current_item.item_sub_page == -1 \
or current_item.item_sub_page == item.item_sub_page:
return
item_pages[current_item.item_sub_page].visible = false
func _init_owniverse_items() -> bool:
owniverse_items = {}
for item_id in Global.OwniverseItems:
var item_props: Dictionary = Global.OwniverseItems[item_id]
var item: OwniverseItem = OWNIVERSE_ITEM.instantiate()
if !item_props.has("page"):
printerr("Item `", item_id, "` has no `page` key.")
return false
var page: int = int(item_props.page)
item_pages[page].add_child(item)
item.init_button(item_props)
item.item_activated.connect(item_activated)
owniverse_items[item_id] = item
return false
func item_activated(item: OwniverseItem) -> void:
_control_page(item)
_set_item_description(item)
if item.item_id == "S" or item.item_id == "H":
owniverse.produce(item.item_id, 100_000)
func _unlock_item(item_id: String):
owniverse_items[item_id].set_visible(true)
func _check_counters(delta: float) -> Array:
var actions: Array = []
for k in g_counters:
var cnt: Counter = g_counters[k]
if not cnt.is_active:
continue
cnt.value += delta
if cnt.value >= cnt.sec:
actions.append(k)
cnt.value = 0
cnt.is_active = false
return actions
#region Swipe
func _do_actions(actions: Array) -> void:
for action: String in actions:
match action:
"swipe":
_swipe_to_nearest_page()
func _swipe_to_nearest_page() -> void:
if stage_page_scroller.scroll_horizontal % PAGE_WIDTH == 0: return
var new_page: int = round(1.0 * stage_page_scroller.scroll_horizontal / PAGE_WIDTH)
if g_tween != null:
if g_tween.is_running():
return
g_tween = stage_page_scroller.get_tree().create_tween()
g_tween.tween_property(stage_page_scroller, "scroll_horizontal",
new_page * PAGE_WIDTH, SWIPE_DURATION).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
func _input(event: InputEvent) -> void:
if event is InputEventScreenDrag:
stage_page_scroller.scroll_horizontal = clamp(
stage_page_scroller.scroll_horizontal - PAGE_COUNT * event.screen_relative.x,
0, PAGE_WIDTH * (PAGE_COUNT - 1))
g_counters.swipe.is_active = true
g_counters.swipe.value = 0
elif event is InputEventPanGesture:
g_counters.swipe.is_active = true
g_counters.swipe.value = 0
elif event is InputEventScreenTouch:
pass
#endregion
func _ready() -> void:
owniverse.init_enitities()
owniverse.load_data()
_init_owniverse_items()
owniverse.unlock_item.connect(_unlock_item)
g_counters.swipe.sec = 0.2
for pa in page_actions:
page_actions[pa].action_pressed.connect(_page_action_pressed)
page_actions[pa].page_id = pa
func _process(delta: float) -> void:
var year_delta: float = owniverse.add_time_delta(delta)
set_datetime(year_delta)
owniverse.cycle(year_delta)
var actions = _check_counters(delta)
_do_actions(actions)
_set_active_page_actions()
func _set_active_page_actions() -> void:
for pa in page_actions:
if !g_active_items.has(pa): continue
var item_id: String = g_active_items[pa].item_id
var amount = owniverse.get_available_to_produce(item_id)
page_actions[pa].set_actions(item_id, amount)
func _page_action_pressed(page_id: int, action_amount: float) -> void:
if !g_active_items.has(page_id):
return
var item_id: String = g_active_items[page_id].item_id
if action_amount > 0:
owniverse.produce(item_id, action_amount)
func _change_speed(delta: int) -> void:
var speed: int = owniverse.change_speed(delta)
var ind: int = 1
for icon in g_speed_icons:
if ind <= speed:
icon.visible = true
else:
icon.visible = false
ind += 1
if speed == 1:
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "2")
elif speed == Global.SPEED_LIMIT:
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(Global.SPEED_LIMIT - 1))
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % "x")
else:
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed - 1))
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
func _on_topmenu_button_pressed(ButtonName) -> void:
print(ButtonName)
#var s: float = 1E15
#var z = 1.0
#for i in range(30):
#print(i, " | ", z, " | ", " | ", s - z)
#z *= 10
func _on_save_data() -> void:
owniverse.save_data()

Some files were not shown because too many files have changed in this diff Show More