ya2 · news · projects · code · about

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