ya2 · news · projects · code · about

5922f4530bce393a789cfab6b9fe497e8dbc11e5
[pmachines.git] / pmachines / items / item.py
1 from panda3d.core import CullFaceAttrib, Point3, Texture, \
2 Plane, Vec3, BitMask32
3 from panda3d.bullet import BulletRigidBodyNode, BulletGhostNode
4 from direct.gui.OnscreenText import OnscreenText
5 from direct.showbase.DirectObject import DirectObject
6 from ya2.utils.gfx import GfxTools, Point
7
8
9 class Command:
10
11 def __init__(self, pos, rot):
12 self.pos = pos
13 self.rot = rot
14
15
16 class ItemStrategy: pass
17
18
19 class FixedStrategy(ItemStrategy):
20
21 def win_condition(self):
22 return True
23
24
25 class StillStrategy(ItemStrategy):
26
27 def __init__(self, np):
28 self._np = np
29 self._positions = []
30 self._rotations = []
31
32 def win_condition(self):
33 self._positions += [self._np.get_pos()]
34 self._rotations += [self._np.get_hpr()]
35 if len(self._positions) > 30:
36 self._positions.pop(0)
37 if len(self._rotations) > 30:
38 self._rotations.pop(0)
39 if len(self._positions) < 28:
40 return
41 avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
42 avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
43 avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
44 avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
45 avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
46 avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
47 avg_pos = Point3(avg_x, avg_y, avg_z)
48 avg_rot = Point3(avg_h, avg_p, avg_r)
49 return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
50 all((rot - avg_rot).length() < 1 for rot in self._rotations)
51
52
53 class Item(DirectObject):
54
55 def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, json, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.5):
56 super().__init__()
57 self._world = world
58 self._plane_node = plane_node
59 self._count = count
60 self._cb_inst = cb_inst
61 self._paused = False
62 self._overlapping = False
63 self._first_command = True
64 self.repos_done = False
65 self._mass = mass
66 self._pos = pos
67 self._r = r
68 self.json = json
69 self.strategy = FixedStrategy()
70 self._exp_num_contacts = exp_num_contacts
71 self._curr_bottom = curr_bottom
72 self._scene_repos = scene_repos
73 self._model_scale = model_scale
74 self._model_path = model_path
75 self._commands = []
76 self._command_idx = -1
77 self._restitution = restitution
78 self._friction = friction
79 self._positions = []
80 self._rotations = []
81 if count:
82 self.node = BulletGhostNode(self.__class__.__name__)
83 else:
84 self.node = BulletRigidBodyNode(self.__class__.__name__)
85 self._set_shape(count)
86 self._np = GfxTools.build_node_from_physics(self.node)
87 if count:
88 world.attach_ghost(self.node)
89 else:
90 world.attach_rigid_body(self.node)
91 self._model = GfxTools.build_model(model_path)
92 self._model.set_srgb_textures()
93 self._model.flatten_light()
94 self._model.reparent_to(self._np)
95 self._np.set_scale(model_scale)
96 self._np.flatten_strong()
97 self._set_outline_model()
98 self._outline_model.set_srgb_textures()
99 self._model.hide(BitMask32(0x01))
100 self._outline_model.hide(BitMask32(0x01))
101 self._start_drag_pos = None
102 self._prev_rot_info = None
103 self._last_nonoverlapping_pos = None
104 self._last_nonoverlapping_rot = None
105 self._instantiated = not count
106 self._box_tsk = taskMgr.add(self.on_frame, 'item_on_frame')
107 if count:
108 self._repos()
109 else:
110 self._np.set_pos(pos)
111 self._np.set_r(r)
112 self.__editing = False
113 self.__interactable = count
114 self.accept('editor-start', self.__editor_start)
115 self.accept('editor-stop', self.__editor_stop)
116
117 def _set_shape(self, apply_scale=True):
118 pass
119
120 def __editor_start(self):
121 self.__editing = True
122
123 def __editor_stop(self):
124 self.__editing = False
125
126 @property
127 def interactable(self):
128 return self.__editing or self.__interactable
129
130 def set_strategy(self, strategy):
131 self.strategy = strategy
132
133 def _repos(self):
134 p_from, p_to = Point((-1, 1)).from_to_points()
135 for hit in self._world.ray_test_all(p_from, p_to).get_hits():
136 if hit.get_node() == self._plane_node:
137 pos = hit.get_hit_pos()
138 #corner = P3dGfxMgr.screen_coord(pos)
139 bounds = self._np.get_tight_bounds()
140 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
141 self._np.set_pos(pos)
142 plane = Plane(Vec3(0, 1, 0), bounds[0][1])
143 pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
144 margin, ar = .04, base.get_aspect_ratio()
145 margin_x = margin / ar if ar >= 1 else margin
146 margin_z = margin * ar if ar < 1 else margin
147 top = self._curr_bottom()
148 base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt)
149 plane.intersects_line(
150 pos3d, render.get_relative_point(base.camera, near_pt),
151 render.get_relative_point(base.camera, far_pt))
152 delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2])
153 self._np.set_pos(pos3d + delta)
154 if not hasattr(self, '_txt'):
155 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
156 font.clear()
157 font.set_pixels_per_unit(60)
158 font.set_minfilter(Texture.FTLinearMipmapLinear)
159 font.set_outline((0, 0, 0, 1), .8, .2)
160 self._txt = OnscreenText(
161 str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1))
162 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
163 p2d = Point(pos).screen_coord()
164 self._txt['pos'] = p2d
165 self.repos_done = True
166
167 def repos_x(self, x):
168 self._np.set_x(x)
169 bounds = self._np.get_tight_bounds()
170 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
171 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
172 p2d = Point(pos).screen_coord()
173 self._txt['pos'] = p2d
174
175 def get_bottom(self):
176 bounds = self._np.get_tight_bounds()
177 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
178 pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2])
179 p2d = Point(pos).screen_coord()
180 ar = base.get_aspect_ratio()
181 return p2d[1] if ar >= 1 else (p2d[1] * ar)
182
183 def get_corner(self):
184 bounds = self._np.get_tight_bounds()
185 return bounds[1][0], bounds[1][1], bounds[0][2]
186
187 def _set_outline_model(self):
188 self._outline_model = GfxTools.build_model(self._model_path)
189 #clockw = CullFaceAttrib.MCullClockwise
190 #self._outline_model.set_attrib(CullFaceAttrib.make(clockw))
191 self._outline_model.set_attrib(CullFaceAttrib.make_reverse())
192 self._outline_model.reparent_to(self._np)
193 self._outline_model.set_scale(1.08)
194 self._outline_model.set_light_off()
195 self._outline_model.set_color(.4, .4, .4, 1)
196 self._outline_model.set_color_scale(.4, .4, .4, 1)
197 self._outline_model.hide()
198
199 def on_frame(self, task):
200 self._np.set_y(0)
201 return task.cont
202
203 def undo(self):
204 self._command_idx -= 1
205 self._np.set_pos(self._commands[self._command_idx].pos)
206 self._np.set_hpr(self._commands[self._command_idx].rot)
207
208 def redo(self):
209 self._command_idx += 1
210 self._np.set_pos(self._commands[self._command_idx].pos)
211 self._np.set_hpr(self._commands[self._command_idx].rot)
212
213 def play(self):
214 if not self._instantiated:
215 return
216 self._world.remove_rigid_body(self.node)
217 self.node.set_mass(self._mass)
218 self._world.attach_rigid_body(self.node)
219 self.node.set_restitution(self._restitution)
220 self.node.set_friction(self._friction)
221
222 def on_click_l(self, pos):
223 if self._paused: return
224 if self.__editing:
225 messenger.send('editor-item-click', [self])
226 self._start_drag_pos = pos, self._np.get_pos()
227 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
228 if not self._instantiated:
229 self._world.remove_ghost(self.node)
230 pos = self._np.get_pos()
231 self._np.remove_node()
232 self.node = BulletRigidBodyNode('box')
233 self._set_shape()
234 self._np = render.attach_new_node(self.node)
235 self._world.attach_rigid_body(self.node)
236 self._model.reparent_to(self._np)
237 self._np.set_pos(pos)
238 self._set_outline_model()
239 self._np.set_scale(self._model_scale)
240 self._model.show(BitMask32(0x01))
241 self._outline_model.hide(BitMask32(0x01))
242 self._instantiated = True
243 self._txt.destroy()
244 self._count -= 1
245 if self._count:
246 item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, None, count=self._count, mass=self._mass, pos=self._pos, r=self._r) # pylint: disable=no-value-for-parameter
247 self._cb_inst(item)
248 self._scene_repos()
249
250 def on_click_r(self, pos):
251 if self._paused or not self._instantiated: return
252 if self.__editing:
253 messenger.send('editor-item-click', [self])
254 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
255 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
256
257 def on_release(self):
258 if self._start_drag_pos or self._prev_rot_info:
259 loader.load_sfx('assets/audio/sfx/release.ogg').play()
260 self._command_idx += 1
261 self._commands = self._commands[:self._command_idx]
262 self._commands += [Command(self._np.get_pos(), self._np.get_hpr())]
263 self._first_command = False
264 self._start_drag_pos = self._prev_rot_info = None
265 if self._overlapping:
266 self._np.set_pos(self._last_nonoverlapping_pos)
267 self._np.set_hpr(self._last_nonoverlapping_rot)
268 self._outline_model.set_color(.4, .4, .4, 1)
269 self._outline_model.set_color_scale(.4, .4, .4, 1)
270 self._overlapping = False
271
272 def on_mouse_on(self):
273 if not self._paused and self.interactable:
274 self._outline_model.show()
275
276 def on_mouse_off(self):
277 if self._start_drag_pos or self._prev_rot_info: return
278 if self.interactable:
279 self._outline_model.hide()
280
281 def on_mouse_move(self, pos):
282 if self._start_drag_pos:
283 d_pos = pos - self._start_drag_pos[0]
284 self._np.set_pos(self._start_drag_pos[1] + d_pos)
285 if self._prev_rot_info:
286 start_vec = self._prev_rot_info[0] - self._prev_rot_info[1]
287 curr_vec = pos - self._prev_rot_info[1]
288 d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0))
289 self._np.set_r(self._prev_rot_info[2] + d_angle)
290 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
291 if self._start_drag_pos or self._prev_rot_info:
292 res = self._world.contact_test(self.node)
293 nres = res.get_num_contacts()
294 if nres <= self._exp_num_contacts:
295 self._overlapping = False
296 self._outline_model.set_color(.4, .4, .4, 1)
297 self._outline_model.set_color_scale(.4, .4, .4, 1)
298 if nres > self._exp_num_contacts and not self._overlapping:
299 actual_nres = 0
300 for contact in res.get_contacts():
301 for node in [contact.get_node0(), contact.get_node1()]:
302 if isinstance(node, BulletRigidBodyNode) and \
303 node != self.node:
304 actual_nres += 1
305 if actual_nres >= 1:
306 self._overlapping = True
307 loader.load_sfx('assets/audio/sfx/overlap.ogg').play()
308 self._outline_model.set_color(.9, .1, .1, 1)
309 self._outline_model.set_color_scale(.9, .1, .1, 1)
310 if not self._overlapping:
311 self._last_nonoverlapping_pos = self._np.get_pos()
312 self._last_nonoverlapping_rot = self._np.get_hpr()
313 messenger.send('item-rototranslated', [self._np])
314
315 def on_aspect_ratio_changed(self):
316 if not self._instantiated:
317 self._repos()
318
319 def store_state(self):
320 self._paused = True
321 self._model.set_transparency(True)
322 self._model.set_alpha_scale(.3)
323 if hasattr(self, '_txt') and not self._txt.is_empty():
324 self._txt.set_alpha_scale(.3)
325
326 def restore_state(self):
327 self._paused = False
328 self._model.set_alpha_scale(1)
329 if hasattr(self, '_txt') and not self._txt.is_empty():
330 self._txt.set_alpha_scale(1)
331
332 def fail_condition(self):
333 if self._np.get_z() < -6:
334 return True
335 self._positions += [self._np.get_pos()]
336 self._rotations += [self._np.get_hpr()]
337 if len(self._positions) > 30:
338 self._positions.pop(0)
339 if len(self._rotations) > 30:
340 self._rotations.pop(0)
341 if len(self._positions) < 28:
342 return
343 avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
344 avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
345 avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
346 avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
347 avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
348 avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
349 avg_pos = Point3(avg_x, avg_y, avg_z)
350 avg_rot = Point3(avg_h, avg_p, avg_r)
351 return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
352 all((rot - avg_rot).length() < 1 for rot in self._rotations)
353
354 @property
355 def mass(self):
356 return self._mass
357
358 @mass.setter
359 def mass(self, val):
360 self._mass = val
361 self.json['mass'] = val
362
363 @property
364 def restitution(self):
365 return self._restitution
366
367 @restitution.setter
368 def restitution(self, val):
369 self._restitution = val
370 self.json['restitution'] = val
371
372 @property
373 def friction(self):
374 return self._friction
375
376 @friction.setter
377 def friction(self, val):
378 self._friction = val
379 self.json['friction'] = val
380
381 @property
382 def position(self):
383 return self._np.get_pos()
384
385 @position.setter
386 def position(self, val):
387 self._np.set_pos(*val)
388 self.json['position'] = val
389
390 @property
391 def roll(self):
392 return self._np.get_r()
393
394 @roll.setter
395 def roll(self, val):
396 self._np.set_r(val)
397 self.json['roll'] = val
398
399 @property
400 def scale(self):
401 return self._np.get_scale()[0]
402
403 @scale.setter
404 def scale(self, val):
405 self._np.set_scale(val)
406 self.json['model_scale'] = val
407
408 @property
409 def id(self):
410 if 'id' in self.json:
411 return self.json['id']
412 return None
413
414 @id.setter
415 def id(self, val):
416 self.json['id'] = val
417
418 @property
419 def strategy_json(self):
420 return self.json['strategy']
421
422 @strategy_json.setter
423 def strategy_json(self, val):
424 self.json['strategy'] = val
425
426 @property
427 def strategy_args_json(self):
428 return self.json['strategy_args']
429
430 @strategy_args_json.setter
431 def strategy_args_json(self, val):
432 self.json['strategy_args'] = val.split(' ')
433
434 def destroy(self):
435 self._np.remove_node()
436 taskMgr.remove(self._box_tsk)
437 if hasattr(self, '_txt'):
438 self._txt.destroy()
439 if not self._instantiated:
440 self._world.remove_ghost(self.node)
441 else:
442 self._world.remove_rigid_body(self.node)
443 self.ignore('editor-start')
444 self.ignore('editor-stop')