aether-engine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aether/__init__.py +54 -0
- aether/audio/__init__.py +0 -0
- aether/audio/audio_manager.py +211 -0
- aether/audio/music.py +6 -0
- aether/audio/sound.py +6 -0
- aether/core/__init__.py +211 -0
- aether/core/achievement_system.py +304 -0
- aether/core/application.py +19 -0
- aether/core/config.py +75 -0
- aether/core/console.py +366 -0
- aether/core/engine.py +50 -0
- aether/core/event_bus.py +24 -0
- aether/core/input.py +58 -0
- aether/core/plugin_system.py +93 -0
- aether/core/save_system.py +409 -0
- aether/core/time_manager.py +25 -0
- aether/core/window.py +57 -0
- aether/graphics/__init__.py +0 -0
- aether/graphics/camera.py +69 -0
- aether/graphics/framebuffer.py +119 -0
- aether/graphics/light.py +32 -0
- aether/graphics/lod_system.py +170 -0
- aether/graphics/material.py +25 -0
- aether/graphics/mesh.py +83 -0
- aether/graphics/mesh_factory.py +67 -0
- aether/graphics/particle_system.py +268 -0
- aether/graphics/post_process.py +279 -0
- aether/graphics/renderer.py +135 -0
- aether/graphics/shader.py +61 -0
- aether/graphics/shader_library.py +157 -0
- aether/graphics/sky_atmosphere.py +309 -0
- aether/graphics/terrain.py +408 -0
- aether/graphics/texture.py +34 -0
- aether/graphics/water.py +432 -0
- aether/physics/__init__.py +0 -0
- aether/physics/bullet_backend.py +395 -0
- aether/physics/collider.py +22 -0
- aether/physics/physics_world.py +436 -0
- aether/physics/raycast.py +25 -0
- aether/physics/rigidbody.py +7 -0
- aether/resources/__init__.py +0 -0
- aether/resources/asset_database.py +76 -0
- aether/resources/resource_manager.py +147 -0
- aether/resources/serialization.py +114 -0
- aether/scene/__init__.py +0 -0
- aether/scene/component.py +368 -0
- aether/scene/entity.py +46 -0
- aether/scene/scene.py +33 -0
- aether/scene/scene_graph.py +252 -0
- aether/scene/scene_manager.py +191 -0
- aether/ui/__init__.py +0 -0
- aether/ui/button.py +297 -0
- aether/ui/canvas.py +11 -0
- aether/ui/text.py +286 -0
- aether/utils/__init__.py +0 -0
- aether/utils/logger.py +16 -0
- aether/utils/math_utils.py +31 -0
- aether/utils/object_pool.py +137 -0
- aether/utils/profiler.py +187 -0
- aether_engine-1.0.0.dist-info/METADATA +71 -0
- aether_engine-1.0.0.dist-info/RECORD +64 -0
- aether_engine-1.0.0.dist-info/WHEEL +5 -0
- aether_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- aether_engine-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Система достижений"""
|
|
2
|
+
from typing import Dict, List, Callable, Any, Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AchievementState(Enum):
|
|
10
|
+
LOCKED = 0
|
|
11
|
+
UNLOCKED = 1
|
|
12
|
+
HIDDEN = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Achievement:
|
|
16
|
+
"""Достижение"""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
achievement_id: str,
|
|
20
|
+
name: str,
|
|
21
|
+
description: str = "",
|
|
22
|
+
icon: str = "",
|
|
23
|
+
hidden: bool = False,
|
|
24
|
+
points: int = 10):
|
|
25
|
+
|
|
26
|
+
self.id = achievement_id
|
|
27
|
+
self.name = name
|
|
28
|
+
self.description = description
|
|
29
|
+
self.icon = icon
|
|
30
|
+
self.hidden = hidden
|
|
31
|
+
self.points = points
|
|
32
|
+
|
|
33
|
+
self.state = AchievementState.HIDDEN if hidden else AchievementState.LOCKED
|
|
34
|
+
self.unlock_time: Optional[float] = None
|
|
35
|
+
self.progress = 0.0 # 0.0 - 1.0
|
|
36
|
+
self.progress_max = 1.0
|
|
37
|
+
|
|
38
|
+
# Условие разблокировки
|
|
39
|
+
self.unlock_condition: Optional[Callable] = None
|
|
40
|
+
|
|
41
|
+
# Статистика
|
|
42
|
+
self.unlock_count = 0
|
|
43
|
+
|
|
44
|
+
def check_unlock(self, context: Dict[str, Any] = None) -> bool:
|
|
45
|
+
"""Проверяет условие разблокировки"""
|
|
46
|
+
if self.state == AchievementState.UNLOCKED:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
if self.unlock_condition:
|
|
50
|
+
try:
|
|
51
|
+
if self.unlock_condition(context or {}):
|
|
52
|
+
self.unlock()
|
|
53
|
+
return True
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def unlock(self):
|
|
60
|
+
"""Разблокирует достижение"""
|
|
61
|
+
if self.state != AchievementState.UNLOCKED:
|
|
62
|
+
self.state = AchievementState.UNLOCKED
|
|
63
|
+
self.unlock_time = time.time()
|
|
64
|
+
self.progress = self.progress_max
|
|
65
|
+
self.unlock_count += 1
|
|
66
|
+
|
|
67
|
+
def update_progress(self, amount: float):
|
|
68
|
+
"""Обновляет прогресс"""
|
|
69
|
+
if self.state == AchievementState.UNLOCKED:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
self.progress = min(self.progress_max, self.progress + amount)
|
|
73
|
+
|
|
74
|
+
if self.progress >= self.progress_max:
|
|
75
|
+
self.unlock()
|
|
76
|
+
|
|
77
|
+
def reset(self):
|
|
78
|
+
"""Сбрасывает достижение"""
|
|
79
|
+
self.state = AchievementState.HIDDEN if self.hidden else AchievementState.LOCKED
|
|
80
|
+
self.unlock_time = None
|
|
81
|
+
self.progress = 0.0
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict:
|
|
84
|
+
return {
|
|
85
|
+
'id': self.id,
|
|
86
|
+
'state': self.state.value,
|
|
87
|
+
'unlock_time': self.unlock_time,
|
|
88
|
+
'progress': self.progress,
|
|
89
|
+
'unlock_count': self.unlock_count,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_dict(cls, data: dict, achievement_def: 'Achievement') -> 'Achievement':
|
|
94
|
+
ach = Achievement(
|
|
95
|
+
achievement_def.id,
|
|
96
|
+
achievement_def.name,
|
|
97
|
+
achievement_def.description,
|
|
98
|
+
achievement_def.icon,
|
|
99
|
+
achievement_def.hidden,
|
|
100
|
+
achievement_def.points
|
|
101
|
+
)
|
|
102
|
+
ach.state = AchievementState(data.get('state', 0))
|
|
103
|
+
ach.unlock_time = data.get('unlock_time')
|
|
104
|
+
ach.progress = data.get('progress', 0.0)
|
|
105
|
+
ach.unlock_count = data.get('unlock_count', 0)
|
|
106
|
+
ach.unlock_condition = achievement_def.unlock_condition
|
|
107
|
+
return ach
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class AchievementCategory:
|
|
111
|
+
"""Категория достижений"""
|
|
112
|
+
|
|
113
|
+
def __init__(self, name: str, icon: str = ""):
|
|
114
|
+
self.name = name
|
|
115
|
+
self.icon = icon
|
|
116
|
+
self.achievements: Dict[str, Achievement] = {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AchievementSystem:
|
|
120
|
+
"""Система достижений"""
|
|
121
|
+
|
|
122
|
+
_instance = None
|
|
123
|
+
|
|
124
|
+
def __new__(cls):
|
|
125
|
+
if cls._instance is None:
|
|
126
|
+
cls._instance = super().__new__(cls)
|
|
127
|
+
cls._instance._initialize()
|
|
128
|
+
return cls._instance
|
|
129
|
+
|
|
130
|
+
def _initialize(self):
|
|
131
|
+
"""Инициализация системы"""
|
|
132
|
+
self._categories: Dict[str, AchievementCategory] = {}
|
|
133
|
+
self._achievements: Dict[str, Achievement] = {}
|
|
134
|
+
self._unlocked_queue: List[Achievement] = []
|
|
135
|
+
|
|
136
|
+
# Статистика игрока
|
|
137
|
+
self.stats: Dict[str, Any] = {
|
|
138
|
+
'play_time': 0.0,
|
|
139
|
+
'deaths': 0,
|
|
140
|
+
'kills': 0,
|
|
141
|
+
'distance_traveled': 0.0,
|
|
142
|
+
'items_collected': 0,
|
|
143
|
+
'quests_completed': 0,
|
|
144
|
+
'total_score': 0,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Коллбэки
|
|
148
|
+
self.on_achievement_unlocked: List[Callable] = []
|
|
149
|
+
|
|
150
|
+
# Сохранение
|
|
151
|
+
self._save_path = Path("./achievements.json")
|
|
152
|
+
|
|
153
|
+
print("[AchievementSystem] Initialized")
|
|
154
|
+
|
|
155
|
+
def create_category(self, name: str, icon: str = "") -> AchievementCategory:
|
|
156
|
+
"""Создаёт категорию"""
|
|
157
|
+
category = AchievementCategory(name, icon)
|
|
158
|
+
self._categories[name] = category
|
|
159
|
+
return category
|
|
160
|
+
|
|
161
|
+
def register_achievement(self,
|
|
162
|
+
achievement_id: str,
|
|
163
|
+
name: str,
|
|
164
|
+
description: str = "",
|
|
165
|
+
category: str = "General",
|
|
166
|
+
icon: str = "",
|
|
167
|
+
hidden: bool = False,
|
|
168
|
+
points: int = 10,
|
|
169
|
+
unlock_condition: Optional[Callable] = None) -> Achievement:
|
|
170
|
+
"""Регистрирует достижение"""
|
|
171
|
+
if category not in self._categories:
|
|
172
|
+
self.create_category(category)
|
|
173
|
+
|
|
174
|
+
achievement = Achievement(
|
|
175
|
+
achievement_id, name, description,
|
|
176
|
+
icon, hidden, points
|
|
177
|
+
)
|
|
178
|
+
achievement.unlock_condition = unlock_condition
|
|
179
|
+
|
|
180
|
+
self._categories[category].achievements[achievement_id] = achievement
|
|
181
|
+
self._achievements[achievement_id] = achievement
|
|
182
|
+
|
|
183
|
+
return achievement
|
|
184
|
+
|
|
185
|
+
def get_achievement(self, achievement_id: str) -> Optional[Achievement]:
|
|
186
|
+
"""Получает достижение по ID"""
|
|
187
|
+
return self._achievements.get(achievement_id)
|
|
188
|
+
|
|
189
|
+
def is_unlocked(self, achievement_id: str) -> bool:
|
|
190
|
+
"""Проверяет, разблокировано ли достижение"""
|
|
191
|
+
ach = self.get_achievement(achievement_id)
|
|
192
|
+
return ach.state == AchievementState.UNLOCKED if ach else False
|
|
193
|
+
|
|
194
|
+
def update_stat(self, stat_name: str, value: Any):
|
|
195
|
+
"""Обновляет статистику"""
|
|
196
|
+
if stat_name in self.stats:
|
|
197
|
+
if isinstance(self.stats[stat_name], (int, float)):
|
|
198
|
+
self.stats[stat_name] += value
|
|
199
|
+
else:
|
|
200
|
+
self.stats[stat_name] = value
|
|
201
|
+
|
|
202
|
+
# Проверяем достижения
|
|
203
|
+
self._check_achievements()
|
|
204
|
+
|
|
205
|
+
def increment_stat(self, stat_name: str, amount: float = 1.0):
|
|
206
|
+
"""Увеличивает статистику"""
|
|
207
|
+
if stat_name in self.stats:
|
|
208
|
+
if isinstance(self.stats[stat_name], (int, float)):
|
|
209
|
+
self.stats[stat_name] += amount
|
|
210
|
+
|
|
211
|
+
self._check_achievements()
|
|
212
|
+
|
|
213
|
+
def _check_achievements(self):
|
|
214
|
+
"""Проверяет все достижения"""
|
|
215
|
+
context = {'stats': self.stats, 'achievements': self._achievements}
|
|
216
|
+
|
|
217
|
+
for achievement in self._achievements.values():
|
|
218
|
+
if achievement.state != AchievementState.UNLOCKED:
|
|
219
|
+
if achievement.check_unlock(context):
|
|
220
|
+
self._unlocked_queue.append(achievement)
|
|
221
|
+
|
|
222
|
+
# Вызываем коллбэки
|
|
223
|
+
for callback in self.on_achievement_unlocked:
|
|
224
|
+
callback(achievement)
|
|
225
|
+
|
|
226
|
+
print(f"[Achievement] Unlocked: {achievement.name} (+{achievement.points} points)")
|
|
227
|
+
|
|
228
|
+
self.stats['total_score'] += achievement.points
|
|
229
|
+
|
|
230
|
+
def get_unlocked_queue(self) -> List[Achievement]:
|
|
231
|
+
"""Возвращает очередь разблокированных достижений"""
|
|
232
|
+
queue = self._unlocked_queue.copy()
|
|
233
|
+
self._unlocked_queue.clear()
|
|
234
|
+
return queue
|
|
235
|
+
|
|
236
|
+
def get_progress(self, achievement_id: str) -> float:
|
|
237
|
+
"""Возвращает прогресс достижения"""
|
|
238
|
+
ach = self.get_achievement(achievement_id)
|
|
239
|
+
return ach.progress / ach.progress_max if ach else 0.0
|
|
240
|
+
|
|
241
|
+
def save(self, path: str = None):
|
|
242
|
+
"""Сохраняет достижения"""
|
|
243
|
+
save_path = Path(path) if path else self._save_path
|
|
244
|
+
|
|
245
|
+
data = {
|
|
246
|
+
'achievements': {},
|
|
247
|
+
'stats': self.stats,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for ach_id, ach in self._achievements.items():
|
|
251
|
+
data['achievements'][ach_id] = ach.to_dict()
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
with open(save_path, 'w') as f:
|
|
255
|
+
json.dump(data, f, indent=2)
|
|
256
|
+
print(f"[AchievementSystem] Saved to {save_path}")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
print(f"[AchievementSystem] Failed to save: {e}")
|
|
259
|
+
|
|
260
|
+
def load(self, path: str = None):
|
|
261
|
+
"""Загружает достижения"""
|
|
262
|
+
load_path = Path(path) if path else self._save_path
|
|
263
|
+
|
|
264
|
+
if not load_path.exists():
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
with open(load_path, 'r') as f:
|
|
269
|
+
data = json.load(f)
|
|
270
|
+
|
|
271
|
+
self.stats = data.get('stats', self.stats)
|
|
272
|
+
|
|
273
|
+
ach_data = data.get('achievements', {})
|
|
274
|
+
for ach_id, ach_state in ach_data.items():
|
|
275
|
+
if ach_id in self._achievements:
|
|
276
|
+
self._achievements[ach_id] = Achievement.from_dict(
|
|
277
|
+
ach_state, self._achievements[ach_id]
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
print(f"[AchievementSystem] Loaded from {load_path}")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"[AchievementSystem] Failed to load: {e}")
|
|
283
|
+
|
|
284
|
+
def reset_all(self):
|
|
285
|
+
"""Сбрасывает все достижения"""
|
|
286
|
+
for achievement in self._achievements.values():
|
|
287
|
+
achievement.reset()
|
|
288
|
+
self.stats = {k: 0 if isinstance(v, (int, float)) else v
|
|
289
|
+
for k, v in self.stats.items()}
|
|
290
|
+
print("[AchievementSystem] All achievements reset")
|
|
291
|
+
|
|
292
|
+
def get_categories(self) -> Dict[str, AchievementCategory]:
|
|
293
|
+
return self._categories.copy()
|
|
294
|
+
|
|
295
|
+
def get_all_achievements(self) -> List[Achievement]:
|
|
296
|
+
return list(self._achievements.values())
|
|
297
|
+
|
|
298
|
+
def get_unlocked_count(self) -> int:
|
|
299
|
+
return sum(1 for ach in self._achievements.values()
|
|
300
|
+
if ach.state == AchievementState.UNLOCKED)
|
|
301
|
+
|
|
302
|
+
def get_total_points(self) -> int:
|
|
303
|
+
return sum(ach.points for ach in self._achievements.values()
|
|
304
|
+
if ach.state == AchievementState.UNLOCKED)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Базовый класс приложения — для быстрого создания игр"""
|
|
2
|
+
from aether.core.engine import Engine
|
|
3
|
+
from aether.scene.scene import Scene
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Application:
|
|
7
|
+
"""Наследуйся от этого класса для создания игры"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, width=1920, height=1080, title="Aether Game"):
|
|
10
|
+
self.engine = Engine(width, height, title)
|
|
11
|
+
self._setup()
|
|
12
|
+
|
|
13
|
+
def _setup(self):
|
|
14
|
+
"""Переопредели этот метод для настройки сцены"""
|
|
15
|
+
scene = Scene("Default Scene")
|
|
16
|
+
self.engine.load_scene(scene)
|
|
17
|
+
|
|
18
|
+
def run(self):
|
|
19
|
+
self.engine.run()
|
aether/core/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Конфигурация движка — централизованные настройки"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field, asdict
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class GraphicsConfig:
|
|
10
|
+
"""Графические настройки"""
|
|
11
|
+
width: int = 1920
|
|
12
|
+
height: int = 1080
|
|
13
|
+
fullscreen: bool = False
|
|
14
|
+
vsync: bool = True
|
|
15
|
+
msaa_samples: int = 4
|
|
16
|
+
shadow_resolution: int = 2048
|
|
17
|
+
shadow_cascades: int = 4
|
|
18
|
+
bloom_enabled: bool = True
|
|
19
|
+
ssao_enabled: bool = True
|
|
20
|
+
hdr_enabled: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PhysicsConfig:
|
|
25
|
+
"""Настройки физики"""
|
|
26
|
+
gravity: tuple = (0.0, -9.81, 0.0)
|
|
27
|
+
fixed_timestep: float = 1.0 / 60.0
|
|
28
|
+
max_substeps: int = 8
|
|
29
|
+
solver_iterations: int = 10
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class AudioConfig:
|
|
34
|
+
"""Настройки звука"""
|
|
35
|
+
master_volume: float = 1.0
|
|
36
|
+
sfx_volume: float = 1.0
|
|
37
|
+
music_volume: float = 0.8
|
|
38
|
+
sample_rate: int = 44100
|
|
39
|
+
channels: int = 32
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class EngineConfig:
|
|
44
|
+
"""Главная конфигурация движка"""
|
|
45
|
+
app_name: str = "Aether Game"
|
|
46
|
+
graphics: GraphicsConfig = field(default_factory=GraphicsConfig)
|
|
47
|
+
physics: PhysicsConfig = field(default_factory=PhysicsConfig)
|
|
48
|
+
audio: AudioConfig = field(default_factory=AudioConfig)
|
|
49
|
+
asset_paths: list = field(default_factory=lambda: ["./assets", "./"])
|
|
50
|
+
|
|
51
|
+
def save(self, path: str):
|
|
52
|
+
"""Сохраняет конфиг в JSON"""
|
|
53
|
+
with open(path, 'w') as f:
|
|
54
|
+
json.dump(asdict(self), f, indent=2)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def load(cls, path: str) -> 'EngineConfig':
|
|
58
|
+
"""Загружает конфиг из JSON"""
|
|
59
|
+
with open(path, 'r') as f:
|
|
60
|
+
data = json.load(f)
|
|
61
|
+
|
|
62
|
+
config = cls()
|
|
63
|
+
config.app_name = data.get('app_name', config.app_name)
|
|
64
|
+
|
|
65
|
+
gfx = data.get('graphics', {})
|
|
66
|
+
config.graphics = GraphicsConfig(**gfx)
|
|
67
|
+
|
|
68
|
+
phys = data.get('physics', {})
|
|
69
|
+
config.physics = PhysicsConfig(**phys)
|
|
70
|
+
|
|
71
|
+
audio = data.get('audio', {})
|
|
72
|
+
config.audio = AudioConfig(**audio)
|
|
73
|
+
|
|
74
|
+
config.asset_paths = data.get('asset_paths', config.asset_paths)
|
|
75
|
+
return config
|