ya2 · news · projects · code · about

6562af21a45070603b56472ad3cef39512604297
[pmachines.git] / pmachines / items / item.py
1 from panda3d.core import CullFaceAttrib, Point3, NodePath, Point2, Texture, \
2 Plane, Vec3, BitMask32
3 from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode, BulletGhostNode
4 from direct.gui.OnscreenText import OnscreenText
5 from lib.lib.p3d.gfx import P3dGfxMgr, set_srgb
6
7
8 class Command:
9
10 def __init__(self, pos, rot):
11 self.pos = pos
12 self.rot = rot
13
14
15 class FixedStrategy:
16
17 def end_condition(self):
18 return True
19
20
21 class StillStrategy:
22
23 def __init__(self, np):
24 self._np = np
25 self._positions = []
26 self._rotations = []
27
28 def end_condition(self):
29 self._positions += [self._np.get_pos()]
30 self._rotations += [self._np.get_hpr()]
31 if len(self._positions) > 10:
32 self._positions.pop(0)
33 if len(self._rotations) > 10:
34 self._rotations.pop(0)
35 if len(self._positions) < 8:
36 return
37 avg_x = sum(pos.x for pos in self._positions) / len(self._positions)
38 avg_y = sum(pos.y for pos in self._positions) / len(self._positions)
39 avg_z = sum(pos.z for pos in self._positions) / len(self._positions)
40 avg_h = sum(rot.x for rot in self._rotations) / len(self._rotations)
41 avg_p = sum(rot.y for rot in self._rotations) / len(self._rotations)
42 avg_r = sum(rot.z for rot in self._rotations) / len(self._rotations)
43 avg_pos = Point3(avg_x, avg_y, avg_z)
44 avg_rot = Point3(avg_h, avg_p, avg_r)
45 return all((pos - avg_pos).length() < .1 for pos in self._positions) and \
46 all((rot - avg_rot).length() < 1 for rot in self._rotations)
47
48
49 class Item:
50
51 def __init__(self, world, plane_node, cb_inst, curr_bottom, scene_repos, model_path, model_scale=1, exp_num_contacts=1, mass=1, pos=(0, 0, 0), r=0, count=0):
52 self._world = world
53 self._plane_node = plane_node
54 self._count = count
55 self._cb_inst = cb_inst
56 self._paused = False
57 self._overlapping = False
58 self._first_command = True
59 self.repos_done = False
60 self._mass = mass
61 self._pos = pos
62 self._r = r
63 self.strategy = FixedStrategy()
64 self._exp_num_contacts = exp_num_contacts
65 self._curr_bottom = curr_bottom
66 self._scene_repos = scene_repos
67 self._model_scale = model_scale
68 self._model_path = model_path
69 self._commands = []
70 self._command_idx = -1
71 if count:
72 self.node = BulletGhostNode(self.__class__.__name__)
73 else:
74 self.node = BulletRigidBodyNode(self.__class__.__name__)
75 self._set_shape()
76 self._np = render.attach_new_node(self.node)
77 if count:
78 world.attach_ghost(self.node)
79 else:
80 world.attach_rigid_body(self.node)
81 self._model = loader.load_model(model_path)
82 set_srgb(self._model)
83 self._model.flatten_light()
84 self._model.reparent_to(self._np)
85 self._np.set_scale(model_scale)
86 if count:
87 self._set_outline_model()
88 set_srgb(self._outline_model)
89 self._model.hide(BitMask32(0x01))
90 self._outline_model.hide(BitMask32(0x01))
91 self._start_drag_pos = None
92 self._prev_rot_info = None
93 self._last_nonoverlapping_pos = None
94 self._last_nonoverlapping_rot = None
95 self._instantiated = not count
96 self.interactable = count
97 self._box_tsk = taskMgr.add(self.on_frame, 'on_frame')
98 if count:
99 self._repos()
100 else:
101 self._np.set_pos(pos)
102 self._np.set_r(r)
103
104 def _set_shape(self):
105 pass
106
107 def set_strategy(self, strategy):
108 self.strategy = strategy
109
110 def _repos(self):
111 p_from, p_to = P3dGfxMgr.world_from_to((-1, 1))
112 for hit in self._world.ray_test_all(p_from, p_to).get_hits():
113 if hit.get_node() == self._plane_node:
114 pos = hit.get_hit_pos()
115 corner = P3dGfxMgr.screen_coord(pos)
116 bounds = self._np.get_tight_bounds()
117 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
118 self._np.set_pos(pos)
119 plane = Plane(Vec3(0, 1, 0), bounds[0][1])
120 pos3d, near_pt, far_pt = Point3(), Point3(), Point3()
121 margin, ar = .04, base.get_aspect_ratio()
122 margin_x = margin / ar if ar >= 1 else margin
123 margin_z = margin * ar if ar < 1 else margin
124 top = self._curr_bottom()
125 base.camLens.extrude((-1 + margin_x, top - margin_z), near_pt, far_pt)
126 plane.intersects_line(
127 pos3d, render.get_relative_point(base.camera, near_pt),
128 render.get_relative_point(base.camera, far_pt))
129 delta = Vec3(bounds[1][0], bounds[1][1], bounds[0][2])
130 self._np.set_pos(pos3d + delta)
131 if not hasattr(self, '_txt'):
132 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
133 font.clear()
134 font.set_pixels_per_unit(60)
135 font.set_minfilter(Texture.FTLinearMipmapLinear)
136 font.set_outline((0, 0, 0, 1), .8, .2)
137 self._txt = OnscreenText(
138 str(self._count), font=font, scale=0.06, fg=(.9, .9, .9, 1))
139 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
140 p2d = P3dGfxMgr.screen_coord(pos)
141 self._txt['pos'] = p2d
142 self.repos_done = True
143
144 def repos_x(self, x):
145 self._np.set_x(x)
146 bounds = self._np.get_tight_bounds()
147 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
148 pos = self._np.get_pos() + (bounds[1][0], bounds[0][1], bounds[0][2])
149 p2d = P3dGfxMgr.screen_coord(pos)
150 self._txt['pos'] = p2d
151
152 def get_bottom(self):
153 bounds = self._np.get_tight_bounds()
154 bounds = bounds[0] - self._np.get_pos(), bounds[1] - self._np.get_pos()
155 pos = self._np.get_pos() + (bounds[1][0], bounds[1][1], bounds[0][2])
156 p2d = P3dGfxMgr.screen_coord(pos)
157 ar = base.get_aspect_ratio()
158 return p2d[1] if ar >= 1 else (p2d[1] * ar)
159
160 def get_corner(self):
161 bounds = self._np.get_tight_bounds()
162 return bounds[1][0], bounds[1][1], bounds[0][2]
163
164 def _set_outline_model(self):
165 self._outline_model = loader.load_model(self._model_path)
166 #clockw = CullFaceAttrib.MCullClockwise
167 #self._outline_model.set_attrib(CullFaceAttrib.make(clockw))
168 self._outline_model.set_attrib(CullFaceAttrib.make_reverse())
169 self._outline_model.reparent_to(self._np)
170 self._outline_model.set_scale(1.08)
171 self._outline_model.set_light_off()
172 self._outline_model.set_color(.4, .4, .4, 1)
173 self._outline_model.set_color_scale(.4, .4, .4, 1)
174 self._outline_model.hide()
175
176 def on_frame(self, task):
177 self._np.set_y(0)
178 return task.cont
179
180 def undo(self):
181 self._command_idx -= 1
182 self._np.set_pos(self._commands[self._command_idx].pos)
183 self._np.set_hpr(self._commands[self._command_idx].rot)
184
185 def redo(self):
186 self._command_idx += 1
187 self._np.set_pos(self._commands[self._command_idx].pos)
188 self._np.set_hpr(self._commands[self._command_idx].rot)
189
190 def play(self):
191 if not self._instantiated:
192 return
193 self._world.remove_rigid_body(self.node)
194 self.node.set_mass(self._mass)
195 self._world.attach_rigid_body(self.node)
196
197 def on_click_l(self, pos):
198 if self._paused: return
199 self._start_drag_pos = pos, self._np.get_pos()
200 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
201 if not self._instantiated:
202 self._world.remove_ghost(self.node)
203 pos = self._np.get_pos()
204 self._np.remove_node()
205 self.node = BulletRigidBodyNode('box')
206 self._set_shape()
207 self._np = render.attach_new_node(self.node)
208 self._world.attach_rigid_body(self.node)
209 self._model.reparent_to(self._np)
210 self._np.set_pos(pos)
211 self._set_outline_model()
212 self._np.set_scale(self._model_scale)
213 self._model.show(BitMask32(0x01))
214 self._outline_model.hide(BitMask32(0x01))
215 self._instantiated = True
216 self._txt.destroy()
217 self._count -= 1
218 if self._count:
219 item = self.__class__(self._world, self._plane_node, self._cb_inst, self._curr_bottom, self._scene_repos, count=self._count, mass=self._mass, pos=self._pos, r=self._r) # pylint: disable=no-value-for-parameter
220 self._cb_inst(item)
221 self._scene_repos()
222
223 def on_click_r(self, pos):
224 if self._paused or not self._instantiated: return
225 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
226 loader.load_sfx('assets/audio/sfx/grab.ogg').play()
227
228 def on_release(self):
229 if self._start_drag_pos or self._prev_rot_info:
230 loader.load_sfx('assets/audio/sfx/release.ogg').play()
231 self._command_idx += 1
232 self._commands = self._commands[:self._command_idx]
233 self._commands += [Command(self._np.get_pos(), self._np.get_hpr())]
234 self._first_command = False
235 self._start_drag_pos = self._prev_rot_info = None
236 if self._overlapping:
237 self._np.set_pos(self._last_nonoverlapping_pos)
238 self._np.set_hpr(self._last_nonoverlapping_rot)
239 self._outline_model.set_color(.4, .4, .4, 1)
240 self._outline_model.set_color_scale(.4, .4, .4, 1)
241 self._overlapping = False
242
243 def on_mouse_on(self):
244 if not self._paused and self.interactable:
245 self._outline_model.show()
246
247 def on_mouse_off(self):
248 if self._start_drag_pos or self._prev_rot_info: return
249 if self.interactable:
250 self._outline_model.hide()
251
252 def on_mouse_move(self, pos):
253 if self._start_drag_pos:
254 d_pos = pos - self._start_drag_pos[0]
255 self._np.set_pos(self._start_drag_pos[1] + d_pos)
256 if self._prev_rot_info:
257 start_vec = self._prev_rot_info[0] - self._prev_rot_info[1]
258 curr_vec = pos - self._prev_rot_info[1]
259 d_angle = curr_vec.signed_angle_deg(start_vec, (0, -1, 0))
260 self._np.set_r(self._prev_rot_info[2] + d_angle)
261 self._prev_rot_info = pos, self._np.get_pos(), self._np.get_r()
262 if self._start_drag_pos or self._prev_rot_info:
263 res = self._world.contact_test(self.node)
264 nres = res.get_num_contacts()
265 if nres <= self._exp_num_contacts:
266 self._overlapping = False
267 self._outline_model.set_color(.4, .4, .4, 1)
268 self._outline_model.set_color_scale(.4, .4, .4, 1)
269 if nres > self._exp_num_contacts and not self._overlapping:
270 actual_nres = 0
271 for contact in res.get_contacts():
272 for node in [contact.get_node0(), contact.get_node1()]:
273 if isinstance(node, BulletRigidBodyNode) and \
274 node != self.node:
275 actual_nres += 1
276 if actual_nres >= 1:
277 self._overlapping = True
278 loader.load_sfx('assets/audio/sfx/overlap.ogg').play()
279 self._outline_model.set_color(.9, .1, .1, 1)
280 self._outline_model.set_color_scale(.9, .1, .1, 1)
281 if not self._overlapping:
282 self._last_nonoverlapping_pos = self._np.get_pos()
283 self._last_nonoverlapping_rot = self._np.get_hpr()
284
285 def on_aspect_ratio_changed(self):
286 if not self._instantiated:
287 self._repos()
288
289 def store_state(self):
290 self._paused = True
291 self._model.set_transparency(True)
292 self._model.set_alpha_scale(.3)
293 if hasattr(self, '_txt') and not self._txt.is_empty():
294 self._txt.set_alpha_scale(.3)
295
296 def restore_state(self):
297 self._paused = False
298 self._model.set_alpha_scale(1)
299 if hasattr(self, '_txt') and not self._txt.is_empty():
300 self._txt.set_alpha_scale(1)
301
302 def destroy(self):
303 self._np.remove_node()
304 taskMgr.remove(self._box_tsk)
305 if hasattr(self, '_txt'):
306 self._txt.destroy()
307 if not self._instantiated:
308 self._world.remove_ghost(self.node)
309 else:
310 self._world.remove_rigid_body(self.node)